From 4d3acc296195cd07e2f85b37cc11ad99f11c109b Mon Sep 17 00:00:00 2001 From: Vitaly Puzrin Date: Mon, 14 Apr 2014 23:30:34 +0400 Subject: [PATCH] Fixes & tests for surrogates support in strings encoder/decoder --- README.md | 8 ++-- lib/utils/strings.js | 88 ++++++++++++++++++++++---------------------- test/strings.js | 71 +++++++++++++++++++++++++++-------- 3 files changed, 104 insertions(+), 63 deletions(-) diff --git a/README.md b/README.md index 3dafd31..fd01f2c 100644 --- a/README.md +++ b/README.md @@ -30,13 +30,13 @@ node v0.10.26, 1mb sample: deflate-gildas x 4.58 ops/sec ±2.33% (15 runs sampled) deflate-imaya x 3.22 ops/sec ±3.95% (12 runs sampled) ! deflate-pako x 6.99 ops/sec ±0.51% (21 runs sampled) - deflate-pako-string x 6.22 ops/sec ±0.76% (19 runs sampled) + deflate-pako-string x 5.89 ops/sec ±0.77% (18 runs sampled) deflate-pako-untyped x 4.39 ops/sec ±1.58% (14 runs sampled) * deflate-zlib x 14.71 ops/sec ±4.23% (59 runs sampled) inflate-dankogai x 32.16 ops/sec ±0.13% (56 runs sampled) inflate-imaya x 30.35 ops/sec ±0.92% (53 runs sampled) ! inflate-pako x 69.89 ops/sec ±1.46% (71 runs sampled) - inflate-pako-string x 64.99 ops/sec ±1.64% (67 runs sampled) + inflate-pako-string x 12.46 ops/sec ±5.65% (36 runs sampled) inflate-pako-untyped x 17.19 ops/sec ±0.85% (32 runs sampled) * inflate-zlib x 70.03 ops/sec ±1.64% (81 runs sampled) @@ -46,13 +46,13 @@ node v0.11.12, 1mb sample: deflate-gildas x 5.06 ops/sec ±6.00% (16 runs sampled) deflate-imaya x 3.52 ops/sec ±3.71% (13 runs sampled) ! deflate-pako x 11.52 ops/sec ±0.22% (32 runs sampled) - deflate-pako-string x 9.61 ops/sec ±0.97% (28 runs sampled) + deflate-pako-string x 9.53 ops/sec ±1.12% (27 runs sampled) deflate-pako-untyped x 5.44 ops/sec ±0.72% (17 runs sampled) * deflate-zlib x 14.05 ops/sec ±3.34% (63 runs sampled) inflate-dankogai x 42.19 ops/sec ±0.09% (56 runs sampled) inflate-imaya x 79.68 ops/sec ±1.07% (68 runs sampled) ! inflate-pako x 97.52 ops/sec ±0.83% (80 runs sampled) - inflate-pako-string x 89.05 ops/sec ±0.48% (76 runs sampled) + inflate-pako-string x 19.07 ops/sec ±12.67% (32 runs sampled) inflate-pako-untyped x 24.35 ops/sec ±2.59% (40 runs sampled) * inflate-zlib x 60.32 ops/sec ±1.36% (69 runs sampled) ``` diff --git a/lib/utils/strings.js b/lib/utils/strings.js index 656fef7..c5bfba6 100644 --- a/lib/utils/strings.js +++ b/lib/utils/strings.js @@ -10,64 +10,66 @@ var STR_APPLY_OK = true; try { String.fromCharCode.apply(null, [0]); } catch(__) { STR_APPLY_OK = false; } -// Table with utf8 lengths -var utf8len = new utils.Buf8(256); +// Table with utf8 lengths (calculated by first byte of sequence) +// Note, that 5 & 6-byte values and some 4-byte values can not be represented in JS, +// because max possible codepoiny is 0x10ffff +var _utf8len = new utils.Buf8(256); for (var i=0; i<256; i++) { - utf8len[i] = (i >= 252 ? 6 : i >= 248 ? 5 : i >= 240 ? 4 : i >= 224 ? 3 : i >= 192 ? 2 : 1); + _utf8len[i] = (i >= 252 ? 6 : i >= 248 ? 5 : i >= 240 ? 4 : i >= 224 ? 3 : i >= 192 ? 2 : 1); } +_utf8len[254]=_utf8len[254]=1; // Invalid sequence start // convert string to array (typed, when possible) // src: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Base64_encoding_and_decoding exports.string2buf = function (str) { - var buf, c, str_len = str.length, buf_len = 0; + var buf, c, c2, m_pos, i, str_len = str.length, buf_len = 0; /* mapping... */ - for (var m_pos = 0; m_pos < str_len; m_pos++) { + for (m_pos = 0; m_pos < str_len; m_pos++) { c = str.charCodeAt(m_pos); - buf_len += c < 0x80 ? 1 : c < 0x800 ? 2 : c < 0x10000 ? 3 : c < 0x200000 ? 4 : c < 0x4000000 ? 5 : 6; + if ((c & 0xfc00) === 0xd800 && (m_pos+1 < str_len)) { + c2 = str.charCodeAt(m_pos+1); + if ((c2 & 0xfc00) === 0xdc00) { + c = 0x10000 + ((c - 0xd800) << 10) + (c2 - 0xdc00); + m_pos++; + } + } + buf_len += c < 0x80 ? 1 : c < 0x800 ? 2 : c < 0x10000 ? 3 : 4; } buf = new utils.Buf8(buf_len); /* transcription... */ - for (var i = 0, c_pos = 0; i < buf_len; c_pos++) { - c = str.charCodeAt(c_pos); - if (c < 128) { + for (i=0, m_pos = 0; i < buf_len; m_pos++) { + c = str.charCodeAt(m_pos); + if ((c & 0xfc00) === 0xd800 && (m_pos+1 < str_len)) { + c2 = str.charCodeAt(m_pos+1); + if ((c2 & 0xfc00) === 0xdc00) { + c = 0x10000 + ((c - 0xd800) << 10) + (c2 - 0xdc00); + m_pos++; + } + } + if (c < 0x80) { /* one byte */ buf[i++] = c; } else if (c < 0x800) { /* two bytes */ - buf[i++] = 192 + (c >>> 6); - buf[i++] = 128 + (c & 63); + buf[i++] = 0xC0 | (c >>> 6); + buf[i++] = 0x80 | (c & 0x3f); } else if (c < 0x10000) { /* three bytes */ - buf[i++] = 224 + (c >>> 12); - buf[i++] = 128 + (c >>> 6 & 63); - buf[i++] = 128 + (c & 63); - } else if (c < 0x200000) { + buf[i++] = 0xE0 | (c >>> 12); + buf[i++] = 0x80 | (c >>> 6 & 0x3f); + buf[i++] = 0x80 | (c & 0x3f); + } else { /* four bytes */ - buf[i++] = 240 + (c >>> 18); - buf[i++] = 128 + (c >>> 12 & 63); - buf[i++] = 128 + (c >>> 6 & 63); - buf[i++] = 128 + (c & 63); - } else if (c < 0x4000000) { - /* five bytes */ - buf[i++] = 248 + (c >>> 24); - buf[i++] = 128 + (c >>> 18 & 63); - buf[i++] = 128 + (c >>> 12 & 63); - buf[i++] = 128 + (c >>> 6 & 63); - buf[i++] = 128 + (c & 63); - } else /* if (c <= 0x7fffffff) */ { - /* six bytes */ - buf[i++] = 252 + /* (c >>> 32) is not possible in ECMAScript! So...: */ (c / 1073741824); - buf[i++] = 128 + (c >>> 24 & 63); - buf[i++] = 128 + (c >>> 18 & 63); - buf[i++] = 128 + (c >>> 12 & 63); - buf[i++] = 128 + (c >>> 6 & 63); - buf[i++] = 128 + (c & 63); + buf[i++] = 0xf0 | (c >>> 18); + buf[i++] = 0x80 | (c >>> 12 & 0x3f); + buf[i++] = 0x80 | (c >>> 6 & 0x3f); + buf[i++] = 0x80 | (c & 0x3f); } } @@ -104,7 +106,7 @@ exports.binstring2buf = function(str) { // src: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Base64_encoding_and_decoding exports.buf2string = function (buf, max) { /*jshint nonstandard:true*/ - // That's not as fast as via String.fromCharCode.appy + // That's not as fast as via String.fromCharCode.apply /*return decodeURIComponent(escape(exports.buf2binstring( (buf.length === max) ? buf @@ -115,24 +117,24 @@ exports.buf2string = function (buf, max) { var str, i, out, part, char_len; var len = max || buf.length; - // Reserve max possibli length + // Reserve max possible length (2 words per char) var utf16buf = new utils.Buf16(len*2); for (out=0, i=0; i len) { utf16buf[out++] = 0xfffd; - break; + break; // end of string reached, stop } switch (char_len) { case 1: utf16buf[out++] = part; break; case 2: - utf16buf[out++] = ((part & 0x1f) << 6) | (buf[++i] & 0x7f); + utf16buf[out++] = ((part & 0x1f) << 6) | (buf[++i] & 0x3f); break; case 3: utf16buf[out++] = ((part & 0x0f) << 12) | ((buf[++i] & 0x3f) << 6) | (buf[++i] & 0x3f); @@ -144,17 +146,15 @@ exports.buf2string = function (buf, max) { utf16buf[out++] = 0xd800 | ((part >> 10) & 0x3ff); utf16buf[out++] = 0xdc00 | (part & 0x3ff); break; - // 5 & 6 bytes uticodes not supported in UTF16 (JS), so fill with dummy symbol + // 5 & 6 bytes uticodes not supported in UTF16 (JS), + // so fill with dummy symbol & update scan position. case 5: i += 4; utf16buf[out++] = 0xfffd; - //utf16buf[out++] = (part - 248 << 24) + (buf[++i] - 128 << 18) + (buf[++i] - 128 << 12) + (buf[++i] - 128 << 6) + buf[++i] - 128; break; case 6: i += 5; utf16buf[out++] = 0xfffd; - // (part - 252 << 32) is not possible in ECMAScript! So...: - //utf16buf[out++] = (part - 252) * 1073741824 + (buf[++i] - 128 << 24) + (buf[++i] - 128 << 18) + (buf[++i] - 128 << 12) + (buf[++i] - 128 << 6) + buf[++i] - 128; break; } } @@ -174,5 +174,5 @@ exports.buf2string = function (buf, max) { // calculate tail size of utf8 char by current byte value exports.utf8tail = function(code) { - return utf8len[code]; + return _utf8len[code]; }; diff --git a/test/strings.js b/test/strings.js index 6880928..d905f40 100644 --- a/test/strings.js +++ b/test/strings.js @@ -9,17 +9,62 @@ var path = require('path'); var assert = require('assert'); var pako = require('../index'); +var cmp = require('./helpers').cmpBuf; +var strings = require('../lib/utils/strings'); -var helpers = require('./helpers'); -var cmp = helpers.cmpBuf; +// fromCharCode, but understands right > 0xffff values +function fixedFromCharCode(code) { + /*jshint bitwise: false*/ + if (code > 0xffff) { + code -= 0x10000; + + var surrogate1 = 0xd800 + (code >> 10) + , surrogate2 = 0xdc00 + (code & 0x3ff); + + return String.fromCharCode(surrogate1, surrogate2); + } else { + return String.fromCharCode(code); + } +} + +// Converts array of codes / chars / strings to utf16 string +function a2utf16(arr) { + var result = ''; + arr.forEach(function (item) { + if (typeof item === 'string') { result += item; return; } + result += fixedFromCharCode(item); + }); + return result; +} -var file = path.join(__dirname, 'fixtures/samples/lorem_utf_100k.txt'); -var sampleString = fs.readFileSync(file, 'utf8'); -var sampleArray = new Uint8Array(fs.readFileSync(file)); +describe('Encode/Decode', function () { + + var utf16sample = a2utf16([0x1f3b5, 'abcd', 0x266a, 0x35, 0xe800, 0x10ffff, 0x0fffff]); + var utf8sample = new Uint8Array(new Buffer(utf16sample)); + + console.log(utf16sample, utf16sample.length); + console.log(new Buffer(utf16sample)); + + it('Encode string to utf8 buf', function () { + assert.ok(cmp( + strings.string2buf(utf16sample), + utf8sample + )); + }); + + it('Decode utf8 buf to string', function () { + assert.ok(strings.buf2string(utf8sample), utf16sample); + }); + +}); -describe('Deflate strings', function () { +describe('Deflate/Inflate strings', function () { + + var file = path.join(__dirname, 'fixtures/samples/lorem_utf_100k.txt'); + var sampleString = fs.readFileSync(file, 'utf8'); + var sampleArray = new Uint8Array(fs.readFileSync(file)); it('Deflate javascript string (utf16) on input', function () { assert.ok(cmp( @@ -35,22 +80,18 @@ describe('Deflate strings', function () { assert.ok(cmp(new Buffer(data, 'binary'), pako.deflate(sampleArray))); }); -}); - - -describe('Inflate strings', function () { - var deflatedString = pako.deflate(sampleArray, { to: 'string' }); - var deflatedArray = pako.deflate(sampleArray); - it('Inflate binary string input', function () { + var deflatedString = pako.deflate(sampleArray, { to: 'string' }); + var deflatedArray = pako.deflate(sampleArray); assert.ok(cmp(pako.inflate(deflatedString), pako.inflate(deflatedArray))); }); it('Inflate with javascript string (utf16) output', function () { + var deflatedArray = pako.deflate(sampleArray); var data = pako.inflate(deflatedArray, { to: 'string', chunkSize: 99 }); assert.equal(typeof data, 'string'); - assert(data === sampleString); + assert.equal(data, sampleString); }); -}); +}); \ No newline at end of file