diff --git a/packages/livedata/base64.js b/packages/livedata/base64.js new file mode 100644 index 0000000000..d46200d73d --- /dev/null +++ b/packages/livedata/base64.js @@ -0,0 +1,127 @@ +// Base 64 encoding + +(function () { + +var BASE_64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +var BASE_64_VALS = {}; + +for (var i = 0; i < BASE_64_CHARS.length; i++) { + BASE_64_VALS[BASE_64_CHARS.substr(i, 1)] = i; +}; + +Meteor._base64Encode = function (array) { + var answer = []; + var a = null; + var b = null; + var c = null; + var d = null; + for (var i = 0; i < array.length; i++) { + switch (i % 3) { + case 0: + a = (array[i] >> 2) & 0x3F; + b = (array[i] & 0x03) << 4; + break; + case 1: + b = b | (array[i] >> 4) & 0xF; + c = (array[i] & 0xF) << 2; + break; + case 2: + c = c | (array[i] >> 6) & 0x03; + d = array[i] & 0x3F; + answer.push(getChar(a)); + answer.push(getChar(b)); + answer.push(getChar(c)); + answer.push(getChar(d)); + a = null; + b = null; + c = null; + d = null; + break; + } + } + if (a != null) { + answer.push(getChar(a)); + answer.push(getChar(b)); + if (c == null) + answer.push('='); + else + answer.push(getChar(c)); + if (d == null) + answer.push('='); + } + return answer.join(""); +}; + +var getChar = function (val) { + return BASE_64_CHARS.substr(val, 1); +}; + +var getVal = function (ch) { + if (ch === '=') { + return -1; + } + return BASE_64_VALS[ch]; +}; + +var newBuffer = function (len) { + if (typeof Uint8Array === 'undefined' || typeof ArrayBuffer === 'undefined') { + var ret = []; + for (var i = 0; i < len; i++) { + ret.push(0); + } + ret.$Uint8ArrayPolyfill = true; + return ret; + } + return new Uint8Array(new ArrayBuffer(len)); +}; + +Meteor._base64Decode = function (str) { + var len = Math.floor((str.length*3)/4); + if (str.substr(str.length - 1) == '=') { + len--; + if (str.substr(str.length - 2, 1) == '=') + len--; + } + var arr = newBuffer(len); + + var one = null; + var two = null; + var three = null; + + var j = 0; + + for (var i = 0; i < str.length; i++) { + var c = str.substr(i, 1); + var v = getVal(c); + switch (i % 4) { + case 0: + if (v < 0) + throw new Error('invalid base64 string'); + one = v << 2; + break; + case 1: + if (v < 0) + throw new Error('invalid base64 string'); + one = one | (v >> 4); + arr[j++] = one; + two = (v & 0x0F) << 4; + break; + case 2: + if (v > 0) { + two = two | (v >> 2); + arr[j++] = two; + three = (v & 0x03) << 6; + } + break; + case 3: + if (v > 0) { + arr[j++] = three | v; + } + break; + } + } + return arr; +}; + +})(); diff --git a/packages/livedata/livedata_common.js b/packages/livedata/livedata_common.js index e1d49fafbc..cb8f8664c9 100644 --- a/packages/livedata/livedata_common.js +++ b/packages/livedata/livedata_common.js @@ -99,6 +99,21 @@ var builtinConverters = [ return new Date(obj.$date); } }, + { // Binary + matchJSONValue: function (obj) { + return _.has(obj, '$binary') && _.size(obj) === 1; + }, + matchObject: function (obj) { + return typeof Uint8Array !== 'undefined' && obj instanceof Uint8Array + || (obj && _.has(obj, '$Uint8ArrayPolyfill')); + }, + toJSONValue: function (obj) { + return {$binary: Meteor._base64Encode(obj)}; + }, + fromJSONValue: function (obj) { + return Meteor._base64Decode(obj.$binary); + } + }, { // Literal matchJSONValue: function (obj) { return _.has(obj, '$literal') && _.size(obj) === 1; diff --git a/packages/livedata/package.js b/packages/livedata/package.js index a741f1790f..7476a600d7 100644 --- a/packages/livedata/package.js +++ b/packages/livedata/package.js @@ -13,6 +13,7 @@ Package.on_use(function (api) { api.add_files('writefence.js', 'server'); api.add_files('crossbar.js', 'server'); + api.add_files('base64.js', ['client', 'server']); api.add_files('livedata_common.js', ['client', 'server']); @@ -35,4 +36,5 @@ Package.on_test(function (api) { api.add_files('livedata_tests.js', ['client', 'server']); api.add_files('livedata_test_service.js', ['client', 'server']); api.add_files('session_view_tests.js', ['server']); + api.add_files('test_base64.js', ['client', 'server']); }); diff --git a/packages/livedata/test_base64.js b/packages/livedata/test_base64.js new file mode 100644 index 0000000000..99c89c8e2c --- /dev/null +++ b/packages/livedata/test_base64.js @@ -0,0 +1,47 @@ +(function () { + + var asciiToArray = function (str) { + var arr = new Uint8Array(new ArrayBuffer(str.length)); + for (var i = 0; i < str.length; i++) { + var c = str.charCodeAt(i); + if (c > 0xFF) { + throw new Error("Not ascii"); + } + arr[i] = c; + } + return arr; + }; + + var arrayToAscii = function (arr) { + var res = []; + for (var i = 0; i < arr.length; i++) { + res.push(String.fromCharCode(arr[i])); + } + return res.join(""); + }; + + Tinytest.add("base64 - testing the test", function (test) { + test.equal(arrayToAscii(asciiToArray("The quick brown fox jumps over the lazy dog")), + "The quick brown fox jumps over the lazy dog"); + }); + + Tinytest.add("base64 - empty", function (test) { + test.equal(Meteor._base64Encode(new Uint8Array(new ArrayBuffer(0))), ""); + test.equal(Meteor._base64Decode(""), new Uint8Array(new ArrayBuffer(0))); + }); + + + Tinytest.add("base64 - wikipedia examples", function (test) { + var tests = [ + {txt: "pleasure.", res: "cGxlYXN1cmUu"}, + {txt: "leasure.", res: "bGVhc3VyZS4="}, + {txt: "easure.", res: "ZWFzdXJlLg=="}, + {txt: "asure.", res: "YXN1cmUu"}, + {txt: "sure.", res: "c3VyZS4="} + ]; + _.each(tests, function(t) { + test.equal(Meteor._base64Encode(asciiToArray(t.txt)), t.res); + test.equal(arrayToAscii(Meteor._base64Decode(t.res)), t.txt); + }); + }); +})();