├── .gitignore ├── LICENSE ├── README.md ├── clone.js ├── index.js ├── package.json ├── serialize.js └── test └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | node_modules 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Technical Machine, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # structured-clone 2 | 3 | Implements the [structured clone algorithm](https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/The_structured_clone_algorithm). Clone JSON types, RegExp, Buffers, and Dates, returning a cloned object or a buffer that can be deserialized into a structured clone 4 | 5 | ``` 6 | npm install structured-clone 7 | ``` 8 | 9 | ```js 10 | var clone = require('structured-clone'); 11 | ``` 12 | 13 | * **clone**(obj) → *Object* 14 | * clone.**serialize**(obj) → *Buffer* 15 | * clone.**deserialize**(buf) → *Object* 16 | 17 | ## Encoded format 18 | 19 | The encoded format takes this form: 20 | 21 | 1. A UTF-8 encoded JSON string. 22 | 2. A null byte. 23 | 3. A binary blob that is the concatenation of all binary buffers in the original object. There are no delimiters in this buffer, indexes are represented in the JSON value (see below). 24 | 25 | Dates, regexps, buffers, and cycles are encoded in a particular way to be decoded properly: 26 | 27 | - Dates are encoded as the string '\x10d' followed by the JSON-stringified encoding of the date. 28 | - Regexps are encoded as the string '\x10r{flags},{regexp source}'. 29 | - Buffers are encoded as the string '\x10b{start},{length}'. All buffers in the encoded value are concatenated and placed in the binary blob. The start and length parameters indicate the indexes the slice of the buffer was encoded in. 30 | - Lastly, string that begin with '\x10' are encoded as '\x10s{string}' to properly escape them. 31 | 32 | **Optimizations:** If only a JSON value is being encoded (i.e. no Buffer values included), the null byte can be omitted, thus making the encoded format equivalent to a JSON-encoded string. If only a buffer is being encoded, it is equivalent to a null byte followed by the buffer (i.e. the JSON string is 0-length). 33 | 34 | ## License 35 | 36 | MIT -------------------------------------------------------------------------------- /clone.js: -------------------------------------------------------------------------------- 1 | // Implement the clone algorithm directly. 2 | // https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/The_structured_clone_algorithm 3 | 4 | function clone (oToBeCloned, cloned, clonedpairs) 5 | { 6 | cloned = cloned || []; 7 | clonedpairs = clonedpairs || []; 8 | 9 | if (cloned.indexOf(oToBeCloned) > -1) { 10 | return clonedpairs[cloned.indexOf(oToBeCloned)]; 11 | } 12 | 13 | if (oToBeCloned === null || !(oToBeCloned instanceof Object)) { return oToBeCloned; } 14 | var oClone, fConstr = oToBeCloned.constructor; 15 | switch (fConstr) { 16 | case RegExp: 17 | oClone = new fConstr(oToBeCloned.source, "g".substr(0, Number(oToBeCloned.global)) + "i".substr(0, Number(oToBeCloned.ignoreCase)) + "m".substr(0, Number(oToBeCloned.multiline))); 18 | break; 19 | case Date: 20 | oClone = new fConstr(oToBeCloned.getTime()); 21 | break; 22 | // etc. 23 | default: 24 | if (Buffer.isBuffer(oToBeCloned)) { 25 | oClone = new Buffer(oToBeCloned.length); 26 | oToBeCloned.copy(oClone); 27 | } else if (oToBeCloned instanceof Error) { 28 | oClone = new Error(oToBeCloned.message); 29 | } else { 30 | oClone = new fConstr(); 31 | cloned.push(oToBeCloned); clonedpairs.push(oClone); 32 | for (var sProp in oToBeCloned) { oClone[sProp] = clone(oToBeCloned[sProp], cloned, clonedpairs); } 33 | } 34 | } 35 | return oClone; 36 | } 37 | 38 | exports.clone = clone; -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Public API. 2 | 3 | module.exports = require('./clone').clone; 4 | module.exports.clone = require('./clone').clone; 5 | module.exports.serialize = require('./serialize').serialize; 6 | module.exports.deserialize = require('./serialize').deserialize; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "structured-clone", 3 | "version": "0.2.2", 4 | "repository": "tessel/structured-clone", 5 | "description": "Implements the structured clone algorithm. Clone JSON types, RegExp, Buffers, and Dates, returning a cloned object or a buffer that can be deserialized into a structured clone.", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "node_modules/.bin/tap test/*.js" 9 | }, 10 | "author": "Tim Cameron Ryan ", 11 | "license": "MIT", 12 | "devDependencies": { 13 | "tap": "~0.4.8", 14 | "tape": "~2.10.2" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /serialize.js: -------------------------------------------------------------------------------- 1 | // Implements serialize/deserialize of structured clones. 2 | // Also resolves cycles (code based on cycle.js by Douglas Crockford) 3 | 4 | function encodeRegExp (regexp) 5 | { 6 | var flags = ''; 7 | if (regexp.global) flags += 'g'; 8 | if (regexp.multiline) flags += 'm'; 9 | if (regexp.ignoreCase) flags += 'i'; 10 | return [flags, regexp.source].join(','); 11 | } 12 | 13 | function decodeRegExp (str) 14 | { 15 | var flags = str.match(/^[^,]*/)[0]; 16 | var source = str.substr(flags.length + 1); 17 | return new RegExp(source, flags); 18 | } 19 | 20 | 21 | // The derez recurses through the object, producing the deep copy. 22 | 23 | function derez(value, path, objects, paths, buffers) 24 | { 25 | if (Buffer.isBuffer(value)) { 26 | var start = Buffer.concat(buffers).length; 27 | buffers.push(value); 28 | return '\x10b' + [start, value.length].join(',') 29 | } 30 | if (value instanceof Date) { 31 | return '\x10d' + value.toJSON(); 32 | } 33 | if (value instanceof RegExp) { 34 | return '\x10r' + encodeRegExp(value); 35 | } 36 | if (value instanceof Error) { 37 | return '\x10e' + value.message 38 | } 39 | if (typeof value == 'string') { 40 | return value.charAt(0) == '\x10' ? '\x10s' + value : value; 41 | } 42 | 43 | var i, // The loop counter 44 | name, // Property name 45 | nu; // The new object or array 46 | 47 | // typeof null === 'object', so go on if this value is really an object but not 48 | // one of the weird builtin objects. 49 | 50 | if (typeof value === 'object' && value !== null && 51 | !(value instanceof Boolean) && 52 | !(value instanceof Number) && 53 | !(value instanceof String)) { 54 | 55 | // If the value is an object or array, look to see if we have already 56 | // encountered it. If so, return a $ref/path object. This is a hard way, 57 | // linear search that will get slower as the number of unique objects grows. 58 | 59 | i = objects.indexOf(value); 60 | if (i !== -1) { 61 | return '\x10j' + paths[i]; 62 | } 63 | 64 | // Otherwise, accumulate the unique value and its path. 65 | 66 | objects.push(value); 67 | paths.push(path); 68 | 69 | // If it is an array, replicate the array. 70 | 71 | if (Array.isArray(value)) { 72 | nu = []; 73 | for (i = 0; i < value.length; i += 1) { 74 | nu[i] = derez(value[i], 75 | path + '[' + i + ']', 76 | objects, paths, buffers); 77 | } 78 | } else { 79 | 80 | // If it is an object, replicate the object. 81 | 82 | nu = {}; 83 | for (name in value) { 84 | if (Object.prototype.hasOwnProperty.call(value, name) && value != '__proto__') { 85 | nu[name] = derez(value[name], 86 | path + '[' + JSON.stringify(name) + ']', 87 | objects, paths, buffers); 88 | } 89 | } 90 | } 91 | return nu; 92 | } 93 | 94 | return value; 95 | } 96 | 97 | 98 | function rerez($, $$) 99 | { 100 | 101 | // Restore an object that was reduced by decycle. Members whose values are 102 | // objects of the form 103 | // {$ref: PATH} 104 | // are replaced with references to the value found by the PATH. This will 105 | // restore cycles. The object will be mutated. 106 | 107 | // The eval function is used to locate the values described by a PATH. The 108 | // root object is kept in a $ variable. A regular expression is used to 109 | // assure that the PATH is extremely well formed. The regexp contains nested 110 | // * quantifiers. That has been known to have extremely bad performance 111 | // problems on some browsers for very long strings. A PATH is expected to be 112 | // reasonably short. A PATH is allowed to belong to a very restricted subset of 113 | // Goessner's JSONPath. 114 | 115 | // So, 116 | // var s = '[{"$ref":"$"}]'; 117 | // return JSON.retrocycle(JSON.parse(s)); 118 | // produces an array containing a single element which is the array itself. 119 | 120 | var px = 121 | /^\${1,4}(?:\[(?:\d+|\"(?:[^\\\"\u0000-\u001f]|\\([\\\"\/bfnrt]|u[0-9a-zA-Z]{4}))*\")\])*$/; 122 | 123 | function redo (item) { 124 | if (typeof item == 'string' && item.charAt(0) == '\x10') { 125 | switch (item.charAt(1)) { 126 | case 's': 127 | return item.substr(2); 128 | case 'b': 129 | var bounds = item.substr(2).split(',', 2); 130 | return $$.slice(parseInt(bounds[0]) || 0, (parseInt(bounds[0]) || 0) + (parseInt(bounds[1]) || [0])); 131 | case 'd': 132 | return new Date(item.substr(2)); 133 | case 'r': 134 | return decodeRegExp(item.substr(2)); 135 | case 'e': 136 | return new Error(item.substr(2)); 137 | case 'j': 138 | var path = item.substr(2); 139 | if (px.test(path)) { 140 | return eval(path); 141 | } 142 | default: return null; 143 | } 144 | } 145 | 146 | if (item && typeof item === 'object') { 147 | rez(item, $$); 148 | } 149 | return item; 150 | } 151 | 152 | function rez(value) { 153 | 154 | // The rez function walks recursively through the object looking for $ref 155 | // properties. When it finds one that has a value that is a path, then it 156 | // replaces the $ref object with a reference to the value that is found by 157 | // the path. 158 | 159 | var i, name; 160 | 161 | if (value && typeof value === 'object') { 162 | if (Array.isArray(value)) { 163 | for (i = 0; i < value.length; i += 1) { 164 | value[i] = redo(value[i]); 165 | } 166 | } else { 167 | for (name in value) { 168 | value[name] = redo(value[name]); 169 | } 170 | } 171 | } 172 | } 173 | 174 | $ = redo($) 175 | 176 | return $; 177 | }; 178 | 179 | 180 | // Public API 181 | 182 | exports.serialize = function (object) { 183 | var objects = [], // Keep a reference to each unique object or array 184 | paths = [], // Keep the path to each unique object or array 185 | buffers = []; // Returned buffers 186 | 187 | var nilbyte = new Buffer([0x00]); 188 | 189 | if (Buffer.isBuffer(object)) { 190 | return Buffer.concat([ 191 | nilbyte, 192 | object 193 | ]); 194 | } 195 | 196 | var json = new Buffer(JSON.stringify(derez(object, '$', objects, paths, buffers))); 197 | if (buffers.length == 0) { 198 | return json; 199 | } 200 | 201 | return Buffer.concat([ 202 | json, 203 | nilbyte, 204 | Buffer.concat(buffers) 205 | ]); 206 | } 207 | 208 | exports.deserialize = function (buf) { 209 | for (var i = 0; i <= buf.length; i++) { 210 | if (buf[i] == 0x00 || buf[i] == null) { 211 | break; 212 | } 213 | } 214 | var jsonbuf = buf.slice(0, i); 215 | var bufbuf = buf.slice(i + 1); 216 | 217 | // Shortcut for only encoding a root buffer. 218 | if (jsonbuf.length == 0) { 219 | return bufbuf; 220 | } 221 | 222 | return rerez(JSON.parse(jsonbuf.toString('utf-8')), bufbuf); 223 | } -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'); 2 | 3 | test('deep cloning test', function (t) { 4 | t.plan(12) 5 | 6 | var clone = require('../') 7 | 8 | var a = { } 9 | a.b = a 10 | a.c = { $ref: '$' } 11 | a.d = new Buffer([0xde, 0xad]) 12 | a.e = [ a, a.b ] 13 | a.f = new Date() 14 | a.g = /ab+a/i 15 | 16 | var a = clone(a); 17 | 18 | t.ok(a.b == a); 19 | t.ok(a.c.$ref == '$') 20 | t.ok(Buffer.isBuffer(a.d)) 21 | t.ok(a.d[0] == 0xde) 22 | t.ok(a.d[1] == 0xad) 23 | t.ok(a.d.length == 2); 24 | t.ok(Array.isArray(a.e)); 25 | t.ok(a.e.length == 2); 26 | t.ok(a.e[0] == a); 27 | t.ok(a.e[1] == a.b); 28 | t.ok(a.f instanceof Date); 29 | t.ok(a.g instanceof RegExp); 30 | }) 31 | 32 | test('serializing test', function (t) { 33 | t.plan(14) 34 | 35 | var clone = require('../') 36 | 37 | var a = { } 38 | a.b = a 39 | a.c = { $ref: '$' } 40 | a.d = new Buffer([0xde, 0xad]) 41 | a.e = [ a, a.b ] 42 | a.f = new Date() 43 | a.g = /ab+a/i 44 | 45 | var buf = clone.serialize(a); 46 | t.ok(buf.length, 'Buffer has length') 47 | t.ok(Buffer.isBuffer(buf), 'Buffer has length') 48 | 49 | var a = clone.deserialize(buf); 50 | 51 | t.ok(a.b == a); 52 | t.ok(a.c.$ref == '$') 53 | t.ok(Buffer.isBuffer(a.d)) 54 | t.ok(a.d[0] == 0xde) 55 | t.ok(a.d[1] == 0xad) 56 | t.ok(a.d.length == 2); 57 | t.ok(Array.isArray(a.e)); 58 | t.ok(a.e.length == 2); 59 | t.ok(a.e[0] == a); 60 | t.ok(a.e[1] == a.b); 61 | t.ok(a.f instanceof Date); 62 | t.ok(a.g instanceof RegExp); 63 | }) 64 | 65 | test('deep copy root object', function (t) { 66 | t.plan(3); 67 | 68 | var clone = require('../'); 69 | 70 | var buf = clone(new Buffer([0xca, 0xfe, 0xba, 0xbe])); 71 | t.ok(Buffer.isBuffer(buf)); 72 | t.ok(buf.length == 4); 73 | t.ok(buf.readUInt32BE(0) == 0xcafebabe); 74 | }); 75 | 76 | test('serializing root object', function (t) { 77 | t.plan(3); 78 | 79 | var clone = require('../'); 80 | 81 | var buf = clone.deserialize(clone.serialize(new Buffer([0xca, 0xfe, 0xba, 0xbe]))); 82 | t.ok(Buffer.isBuffer(buf)); 83 | t.ok(buf.length == 4); 84 | t.ok(buf.readUInt32BE(0) == 0xcafebabe); 85 | }); 86 | 87 | 88 | test('errors', function (t) { 89 | t.plan(4); 90 | 91 | var clone = require('../'); 92 | 93 | var err = clone(new Error('boo!')); 94 | t.ok(err instanceof Error); 95 | t.ok(err.message == 'boo!'); 96 | 97 | var err = clone.deserialize(clone.serialize(new Error('boo!'))); 98 | t.ok(err instanceof Error); 99 | t.ok(err.message == 'boo!'); 100 | }); 101 | 102 | test('arrays of buffers', function(t) { 103 | t.plan(5); 104 | 105 | var b1 = new Buffer([0, 1, 2, 3, 4, 5]); 106 | var b2 = new Buffer([6, 7, 8, 9, 10, 11]); 107 | var arr = [b1, b2]; 108 | 109 | var clone = require('../'); 110 | 111 | var buf = clone.deserialize(clone.serialize(arr)); 112 | 113 | t.ok(buf.length === arr.length); 114 | t.ok(buf[0].length === arr[0].length); 115 | t.ok(buf[1].length === arr[0].length); 116 | 117 | function bufferEqual (a, b) { 118 | for (var i = 0; i < a.length; i++) { 119 | if (a[i] !== b[i]) return false; 120 | } 121 | return true; 122 | } 123 | 124 | t.ok(bufferEqual(buf[0], arr[0])); 125 | t.ok(bufferEqual(buf[1], arr[1])); 126 | 127 | }) --------------------------------------------------------------------------------