diff --git a/core/httpd.js b/core/httpd.js index fdbcb647..c9d0f051 100644 --- a/core/httpd.js +++ b/core/httpd.js @@ -1,5 +1,7 @@ "use strict"; +require("stringview"); + var gHandlers = []; var gSocketHandlers = []; @@ -185,7 +187,7 @@ function handleRequest(request, response) { } function handleWebSocketRequest(request, response, client) { - var buffer = ""; + var buffer = new Uint8Array(0); var frame = ""; var frameOpCode = 0x0; @@ -199,24 +201,30 @@ function handleWebSocketRequest(request, response, client) { if (opCode === undefined) { opCode = 0x2; } + if (opCode == 0x1 && (typeof message == "string" || message instanceof String)) { + message = new StringView(message, "UTF-8").rawData; + } var fin = true; - var packet = String.fromCharCode((fin ? (1 << 7) : 0) | (opCode & 0xf)); + var packet = [(fin ? (1 << 7) : 0) | (opCode & 0xf)]; var mask = false; if (message.length < 126) { - packet += String.fromCharCode((mask ? (1 << 7) : 0) | message.length); + packet.push((mask ? (1 << 7) : 0) | message.length); } else if (message.length < (1 << 16)) { - packet += String.fromCharCode((mask ? (1 << 7) : 0) | 126); - packet += String.fromCharCode((message.length >> 8) & 0xff); - packet += String.fromCharCode(message.length & 0xff); + packet.push((mask ? (1 << 7) : 0) | 126); + packet.push((message.length >> 8) & 0xff); + packet.push(message.length & 0xff); } else { - packet += String.fromCharCode((mask ? (1 << 7) : 0) | 127); - packet += String.fromCharCode((message.length >> 24) & 0xff); - packet += String.fromCharCode((message.length >> 16) & 0xff); - packet += String.fromCharCode((message.length >> 8) & 0xff); - packet += String.fromCharCode(message.length & 0xff); + packet.push((mask ? (1 << 7) : 0) | 127); + packet.push((message.length >> 24) & 0xff); + packet.push((message.length >> 16) & 0xff); + packet.push((message.length >> 8) & 0xff); + packet.push(message.length & 0xff); } - packet += message; - return client.write(packet); + + var array = new Uint8Array(packet.length + message.length); + array.set(packet, 0); + array.set(message, packet.length); + return client.write(array); } response.onMessage = null; @@ -224,10 +232,14 @@ function handleWebSocketRequest(request, response, client) { client.read(function(data) { if (data) { - buffer += data; + var newBuffer = new Uint8Array(buffer.length + data.length); + newBuffer.set(buffer, 0); + newBuffer.set(data, buffer.length); + buffer = newBuffer; + if (buffer.length >= 2) { - var bits0 = buffer.charCodeAt(0); - var bits1 = buffer.charCodeAt(1); + var bits0 = buffer[0]; + var bits1 = buffer[1]; if (bits1 & (1 << 7) == 0) { // Unmasked message. client.close(); @@ -241,26 +253,26 @@ function handleWebSocketRequest(request, response, client) { payloadLength = 0; for (var i = 0; i < 2; i++) { payloadLength <<= 8; - payloadLength |= buffer.charCodeAt(2 + i); + payloadLength |= buffer[2 + i]; } maskStart = 4; } else if (payloadLength == 127) { payloadLength = 0; for (var i = 0; i < 8; i++) { payloadLength <<= 8; - payloadLength |= buffer.charCodeAt(2 + i); + payloadLength |= buffer[2 + i]; } maskStart = 10; } var havePayload = buffer.length >= payloadLength + 2 + 4; if (havePayload) { - var mask = buffer.substring(maskStart, maskStart + 4); + var mask = buffer.slice(maskStart, maskStart + 4); var dataStart = maskStart + 4; var decoded = ""; - var payload = buffer.substring(dataStart, dataStart + payloadLength); - buffer = buffer.substring(dataStart + payloadLength); + var payload = buffer.slice(dataStart, dataStart + payloadLength); + buffer = buffer.slice(dataStart + payloadLength); for (var i = 0; i < payloadLength; i++) { - decoded += String.fromCharCode(payload.charCodeAt(i) ^ mask.charCodeAt(i % 4)); + decoded += String.fromCharCode(payload[i] ^ mask[i % 4]); } frame += decoded; @@ -319,7 +331,7 @@ function webSocketAcceptResponse(key) { } function handleConnection(client) { - var inputBuffer = ""; + var inputBuffer = new Uint8Array(0); var request; var headers = {}; var lineByLine = true; @@ -327,7 +339,7 @@ function handleConnection(client) { var body; function reset() { - inputBuffer = ""; + inputBuffer = new Uint8Array(0); request = undefined; headers = {}; lineByLine = true; @@ -350,6 +362,7 @@ function handleConnection(client) { function handleLine(line, length) { if (bodyToRead == -1) { + line = new StringView(line, "ASCII").toString(); if (!request) { request = line.split(' '); return true; @@ -379,6 +392,7 @@ function handleConnection(client) { } } } else { + line = new StringView(line, "UTF-8").toString(); body += line; bodyToRead -= length; if (bodyToRead <= 0) { @@ -393,14 +407,21 @@ function handleConnection(client) { client.read(function(data) { if (data) { - inputBuffer += data; + var newBuffer = new Uint8Array(inputBuffer.length + data.length); + newBuffer.set(inputBuffer, 0); + newBuffer.set(data, inputBuffer.length); + inputBuffer = newBuffer; + + var newLine = '\n'.charCodeAt(0); + var carriageReturn = '\r'.charCodeAt(0); + var more = true; while (more) { if (lineByLine) { more = false; - var end = inputBuffer.indexOf('\n'); + var end = inputBuffer.indexOf(newLine); var realEnd = end; - if (end > 0 && inputBuffer[end - 1] == '\r') { + if (end > 0 && inputBuffer[end - 1] == carriageReturn) { --end; } if (end != -1) { @@ -410,7 +431,7 @@ function handleConnection(client) { } } else { more = handleLine(inputBuffer, inputBuffer.length); - inputBuffer = ""; + inputBuffer = new Uint8Array(0); } } } diff --git a/core/stringview.js b/core/stringview.js new file mode 100644 index 00000000..7f73143d --- /dev/null +++ b/core/stringview.js @@ -0,0 +1,679 @@ +"use strict"; + +/*\ +|*| +|*| :: Number.isInteger() polyfill :: +|*| +|*| https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isInteger +|*| +\*/ + +if (!Number.isInteger) { + Number.isInteger = function isInteger (nVal) { + return typeof nVal === "number" && isFinite(nVal) && nVal > -9007199254740992 && nVal < 9007199254740992 && Math.floor(nVal) === nVal; + }; +} + +/*\ +|*| +|*| StringView - Mozilla Developer Network +|*| +|*| Revision #9, October 30, 2016 +|*| +|*| https://developer.mozilla.org/en-US/Add-ons/Code_snippets/StringView +|*| https://developer.mozilla.org/en-US/docs/User:fusionchess +|*| https://github.com/madmurphy/stringview.js +|*| +|*| This framework is released under the GNU Lesser General Public License, version 3 or later. +|*| http://www.gnu.org/licenses/lgpl-3.0.html +|*| +\*/ + +function StringView (vInput, sEncoding /* optional (default: UTF-8) */, nOffset /* optional */, nLength /* optional */) { + + var fTAView, aWhole, aRaw, fPutOutptCode, fGetOutptChrSize, nInptLen, nStartIdx = isFinite(nOffset) ? nOffset : 0, nTranscrType = 15; + + if (sEncoding) { this.encoding = sEncoding.toString(); } + + encSwitch: switch (this.encoding) { + case "UTF-8": + fPutOutptCode = StringView.putUTF8CharCode; + fGetOutptChrSize = StringView.getUTF8CharLength; + fTAView = Uint8Array; + break encSwitch; + case "UTF-16": + fPutOutptCode = StringView.putUTF16CharCode; + fGetOutptChrSize = StringView.getUTF16CharLength; + fTAView = Uint16Array; + break encSwitch; + case "UTF-32": + fTAView = Uint32Array; + nTranscrType &= 14; + break encSwitch; + default: + /* case "ASCII", or case "BinaryString" or unknown cases */ + fTAView = Uint8Array; + nTranscrType &= 14; + } + + typeSwitch: switch (typeof vInput) { + case "string": + /* the input argument is a primitive string: a new buffer will be created. */ + nTranscrType &= 7; + break typeSwitch; + case "object": + classSwitch: switch (vInput.constructor) { + case StringView: + /* the input argument is a stringView: a new buffer will be created. */ + nTranscrType &= 3; + break typeSwitch; + case String: + /* the input argument is an objectified string: a new buffer will be created. */ + nTranscrType &= 7; + break typeSwitch; + case ArrayBuffer: + /* the input argument is an arrayBuffer: the buffer will be shared. */ + aWhole = new fTAView(vInput); + nInptLen = this.encoding === "UTF-32" ? + vInput.byteLength >>> 2 + : this.encoding === "UTF-16" ? + vInput.byteLength >>> 1 + : + vInput.byteLength; + aRaw = nStartIdx === 0 && (!isFinite(nLength) || nLength === nInptLen) ? + aWhole + : new fTAView(vInput, nStartIdx, !isFinite(nLength) ? nInptLen - nStartIdx : nLength); + + break typeSwitch; + case Uint32Array: + case Uint16Array: + case Uint8Array: + /* the input argument is a typedArray: the buffer, and possibly the array itself, will be shared. */ + fTAView = vInput.constructor; + nInptLen = vInput.length; + aWhole = vInput.byteOffset === 0 && vInput.length === ( + fTAView === Uint32Array ? + vInput.buffer.byteLength >>> 2 + : fTAView === Uint16Array ? + vInput.buffer.byteLength >>> 1 + : + vInput.buffer.byteLength + ) ? vInput : new fTAView(vInput.buffer); + aRaw = nStartIdx === 0 && (!isFinite(nLength) || nLength === nInptLen) ? + vInput + : vInput.subarray(nStartIdx, isFinite(nLength) ? nStartIdx + nLength : nInptLen); + + break typeSwitch; + default: + /* the input argument is an array or another serializable object: a new typedArray will be created. */ + aWhole = new fTAView(vInput); + nInptLen = aWhole.length; + aRaw = nStartIdx === 0 && (!isFinite(nLength) || nLength === nInptLen) ? + aWhole + : aWhole.subarray(nStartIdx, isFinite(nLength) ? nStartIdx + nLength : nInptLen); + } + break typeSwitch; + default: + /* the input argument is a number, a boolean or a function: a new typedArray will be created. */ + aWhole = aRaw = new fTAView(Number(vInput) || 0); + + } + + if (nTranscrType < 8) { + + var vSource, nOutptLen, nCharStart, nCharEnd, nEndIdx, fGetInptChrSize, fGetInptChrCode; + + if (nTranscrType & 4) { /* input is string */ + + vSource = vInput; + nOutptLen = nInptLen = vSource.length; + nTranscrType ^= this.encoding === "UTF-32" ? 0 : 2; + /* ...or...: nTranscrType ^= Number(this.encoding !== "UTF-32") << 1; */ + nStartIdx = nCharStart = nOffset ? Math.max((nOutptLen + nOffset) % nOutptLen, 0) : 0; + nEndIdx = nCharEnd = (Number.isInteger(nLength) ? Math.min(Math.max(nLength, 0) + nStartIdx, nOutptLen) : nOutptLen) - 1; + + } else { /* input is stringView */ + + vSource = vInput.rawData; + nInptLen = vInput.makeIndex(); + nStartIdx = nCharStart = nOffset ? Math.max((nInptLen + nOffset) % nInptLen, 0) : 0; + nOutptLen = Number.isInteger(nLength) ? Math.min(Math.max(nLength, 0), nInptLen - nCharStart) : nInptLen; + nEndIdx = nCharEnd = nOutptLen + nCharStart; + + if (vInput.encoding === "UTF-8") { + fGetInptChrSize = StringView.getUTF8CharLength; + fGetInptChrCode = StringView.loadUTF8CharCode; + } else if (vInput.encoding === "UTF-16") { + fGetInptChrSize = StringView.getUTF16CharLength; + fGetInptChrCode = StringView.loadUTF16CharCode; + } else { + nTranscrType &= 1; + } + + } + + if (nOutptLen === 0 || nTranscrType < 4 && vSource.encoding === this.encoding && nCharStart === 0 && nOutptLen === nInptLen) { + + /* the encoding is the same, the length too and the offset is 0... or the input is empty! */ + + nTranscrType = 7; + + } + + conversionSwitch: switch (nTranscrType) { + + case 0: + + /* both the source and the new StringView have a fixed-length encoding... */ + + aWhole = new fTAView(nOutptLen); + for (var nOutptIdx = 0; nOutptIdx < nOutptLen; aWhole[nOutptIdx] = vSource[nStartIdx + nOutptIdx++]); + break conversionSwitch; + + case 1: + + /* the source has a fixed-length encoding but the new StringView has a variable-length encoding... */ + + /* mapping... */ + + nOutptLen = 0; + + for (var nInptIdx = nStartIdx; nInptIdx < nEndIdx; nInptIdx++) { + nOutptLen += fGetOutptChrSize(vSource[nInptIdx]); + } + + aWhole = new fTAView(nOutptLen); + + /* transcription of the source... */ + + for (var nInptIdx = nStartIdx, nOutptIdx = 0; nOutptIdx < nOutptLen; nInptIdx++) { + nOutptIdx = fPutOutptCode(aWhole, vSource[nInptIdx], nOutptIdx); + } + + break conversionSwitch; + + case 2: + + /* the source has a variable-length encoding but the new StringView has a fixed-length encoding... */ + + /* mapping... */ + + nStartIdx = 0; + + var nChrCode; + + for (nChrIdx = 0; nChrIdx < nCharStart; nChrIdx++) { + nChrCode = fGetInptChrCode(vSource, nStartIdx); + nStartIdx += fGetInptChrSize(nChrCode); + } + + aWhole = new fTAView(nOutptLen); + + /* transcription of the source... */ + + for (var nInptIdx = nStartIdx, nOutptIdx = 0; nOutptIdx < nOutptLen; nInptIdx += fGetInptChrSize(nChrCode), nOutptIdx++) { + nChrCode = fGetInptChrCode(vSource, nInptIdx); + aWhole[nOutptIdx] = nChrCode; + } + + break conversionSwitch; + + case 3: + + /* both the source and the new StringView have a variable-length encoding... */ + + /* mapping... */ + + nOutptLen = 0; + + var nChrCode; + + for (var nChrIdx = 0, nInptIdx = 0; nChrIdx < nCharEnd; nInptIdx += fGetInptChrSize(nChrCode)) { + nChrCode = fGetInptChrCode(vSource, nInptIdx); + if (nChrIdx === nCharStart) { nStartIdx = nInptIdx; } + if (++nChrIdx > nCharStart) { nOutptLen += fGetOutptChrSize(nChrCode); } + } + + aWhole = new fTAView(nOutptLen); + + /* transcription... */ + + for (var nInptIdx = nStartIdx, nOutptIdx = 0; nOutptIdx < nOutptLen; nInptIdx += fGetInptChrSize(nChrCode)) { + nChrCode = fGetInptChrCode(vSource, nInptIdx); + nOutptIdx = fPutOutptCode(aWhole, nChrCode, nOutptIdx); + } + + break conversionSwitch; + + case 4: + + /* DOMString to ASCII or BinaryString or other unknown encodings */ + + aWhole = new fTAView(nOutptLen); + + /* transcription... */ + + for (var nIdx = 0; nIdx < nOutptLen; nIdx++) { + aWhole[nIdx] = vSource.charCodeAt(nIdx) & 0xff; + } + + break conversionSwitch; + + case 5: + + /* DOMString to UTF-8 or to UTF-16 */ + + /* mapping... */ + + nOutptLen = 0; + + for (var nMapIdx = 0; nMapIdx < nInptLen; nMapIdx++) { + if (nMapIdx === nCharStart) { nStartIdx = nOutptLen; } + nOutptLen += fGetOutptChrSize(vSource.charCodeAt(nMapIdx)); + if (nMapIdx === nCharEnd) { nEndIdx = nOutptLen; } + } + + aWhole = new fTAView(nOutptLen); + + /* transcription... */ + + for (var nOutptIdx = 0, nChrIdx = 0; nOutptIdx < nOutptLen; nChrIdx++) { + nOutptIdx = fPutOutptCode(aWhole, vSource.charCodeAt(nChrIdx), nOutptIdx); + } + + break conversionSwitch; + + case 6: + + /* DOMString to UTF-32 */ + + aWhole = new fTAView(nOutptLen); + + /* transcription... */ + + for (var nIdx = 0; nIdx < nOutptLen; nIdx++) { + aWhole[nIdx] = vSource.charCodeAt(nIdx); + } + + break conversionSwitch; + + case 7: + + aWhole = new fTAView(nOutptLen ? vSource : 0); + break conversionSwitch; + + } + + aRaw = nTranscrType > 3 && (nStartIdx > 0 || nEndIdx < aWhole.length - 1) ? aWhole.subarray(nStartIdx, nEndIdx) : aWhole; + + } + + this.buffer = aWhole.buffer; + this.bufferView = aWhole; + this.rawData = aRaw; + + Object.freeze(this); + +} + +/* CONSTRUCTOR'S METHODS */ + +StringView.loadUTF8CharCode = function (aChars, nIdx) { + + var nLen = aChars.length, nPart = aChars[nIdx]; + + return nPart > 251 && nPart < 254 && nIdx + 5 < nLen ? + /* (nPart - 252 << 30) may be not safe in ECMAScript! So...: */ + /* six bytes */ (nPart - 252) * 1073741824 + (aChars[nIdx + 1] - 128 << 24) + (aChars[nIdx + 2] - 128 << 18) + (aChars[nIdx + 3] - 128 << 12) + (aChars[nIdx + 4] - 128 << 6) + aChars[nIdx + 5] - 128 + : nPart > 247 && nPart < 252 && nIdx + 4 < nLen ? + /* five bytes */ (nPart - 248 << 24) + (aChars[nIdx + 1] - 128 << 18) + (aChars[nIdx + 2] - 128 << 12) + (aChars[nIdx + 3] - 128 << 6) + aChars[nIdx + 4] - 128 + : nPart > 239 && nPart < 248 && nIdx + 3 < nLen ? + /* four bytes */(nPart - 240 << 18) + (aChars[nIdx + 1] - 128 << 12) + (aChars[nIdx + 2] - 128 << 6) + aChars[nIdx + 3] - 128 + : nPart > 223 && nPart < 240 && nIdx + 2 < nLen ? + /* three bytes */ (nPart - 224 << 12) + (aChars[nIdx + 1] - 128 << 6) + aChars[nIdx + 2] - 128 + : nPart > 191 && nPart < 224 && nIdx + 1 < nLen ? + /* two bytes */ (nPart - 192 << 6) + aChars[nIdx + 1] - 128 + : + /* one byte */ nPart; + +}; + +StringView.putUTF8CharCode = function (aTarget, nChar, nPutAt) { + + var nIdx = nPutAt; + + if (nChar < 0x80 /* 128 */) { + /* one byte */ + aTarget[nIdx++] = nChar; + } else if (nChar < 0x800 /* 2048 */) { + /* two bytes */ + aTarget[nIdx++] = 0xc0 /* 192 */ + (nChar >>> 6); + aTarget[nIdx++] = 0x80 /* 128 */ + (nChar & 0x3f /* 63 */); + } else if (nChar < 0x10000 /* 65536 */) { + /* three bytes */ + aTarget[nIdx++] = 0xe0 /* 224 */ + (nChar >>> 12); + aTarget[nIdx++] = 0x80 /* 128 */ + ((nChar >>> 6) & 0x3f /* 63 */); + aTarget[nIdx++] = 0x80 /* 128 */ + (nChar & 0x3f /* 63 */); + } else if (nChar < 0x200000 /* 2097152 */) { + /* four bytes */ + aTarget[nIdx++] = 0xf0 /* 240 */ + (nChar >>> 18); + aTarget[nIdx++] = 0x80 /* 128 */ + ((nChar >>> 12) & 0x3f /* 63 */); + aTarget[nIdx++] = 0x80 /* 128 */ + ((nChar >>> 6) & 0x3f /* 63 */); + aTarget[nIdx++] = 0x80 /* 128 */ + (nChar & 0x3f /* 63 */); + } else if (nChar < 0x4000000 /* 67108864 */) { + /* five bytes */ + aTarget[nIdx++] = 0xf8 /* 248 */ + (nChar >>> 24); + aTarget[nIdx++] = 0x80 /* 128 */ + ((nChar >>> 18) & 0x3f /* 63 */); + aTarget[nIdx++] = 0x80 /* 128 */ + ((nChar >>> 12) & 0x3f /* 63 */); + aTarget[nIdx++] = 0x80 /* 128 */ + ((nChar >>> 6) & 0x3f /* 63 */); + aTarget[nIdx++] = 0x80 /* 128 */ + (nChar & 0x3f /* 63 */); + } else /* if (nChar <= 0x7fffffff) */ { /* 2147483647 */ + /* six bytes */ + aTarget[nIdx++] = 0xfc /* 252 */ + /* (nChar >>> 30) may be not safe in ECMAScript! So...: */ (nChar / 1073741824); + aTarget[nIdx++] = 0x80 /* 128 */ + ((nChar >>> 24) & 0x3f /* 63 */); + aTarget[nIdx++] = 0x80 /* 128 */ + ((nChar >>> 18) & 0x3f /* 63 */); + aTarget[nIdx++] = 0x80 /* 128 */ + ((nChar >>> 12) & 0x3f /* 63 */); + aTarget[nIdx++] = 0x80 /* 128 */ + ((nChar >>> 6) & 0x3f /* 63 */); + aTarget[nIdx++] = 0x80 /* 128 */ + (nChar & 0x3f /* 63 */); + } + + return nIdx; + +}; + +StringView.getUTF8CharLength = function (nChar) { + return nChar < 0x80 ? 1 : nChar < 0x800 ? 2 : nChar < 0x10000 ? 3 : nChar < 0x200000 ? 4 : nChar < 0x4000000 ? 5 : 6; +}; + +StringView.loadUTF16CharCode = function (aChars, nIdx) { + + /* UTF-16 to DOMString decoding algorithm */ + var nFrstChr = aChars[nIdx]; + + return nFrstChr > 0xD7BF /* 55231 */ && nIdx + 1 < aChars.length ? + (nFrstChr - 0xD800 /* 55296 */ << 10) + aChars[nIdx + 1] + 0x2400 /* 9216 */ + : nFrstChr; + +}; + +StringView.putUTF16CharCode = function (aTarget, nChar, nPutAt) { + + var nIdx = nPutAt; + + if (nChar < 0x10000 /* 65536 */) { + /* one element */ + aTarget[nIdx++] = nChar; + } else { + /* two elements */ + aTarget[nIdx++] = 0xD7C0 /* 55232 */ + (nChar >>> 10); + aTarget[nIdx++] = 0xDC00 /* 56320 */ + (nChar & 0x3FF /* 1023 */); + } + + return nIdx; + +}; + +StringView.getUTF16CharLength = function (nChar) { + return nChar < 0x10000 ? 1 : 2; +}; + +/* Array of bytes to base64 string decoding */ + +StringView.b64ToUint6 = function (nChr) { + + return nChr > 64 && nChr < 91 ? + nChr - 65 + : nChr > 96 && nChr < 123 ? + nChr - 71 + : nChr > 47 && nChr < 58 ? + nChr + 4 + : nChr === 43 ? + 62 + : nChr === 47 ? + 63 + : + 0; + +}; + +StringView.uint6ToB64 = function (nUint6) { + + return nUint6 < 26 ? + nUint6 + 65 + : nUint6 < 52 ? + nUint6 + 71 + : nUint6 < 62 ? + nUint6 - 4 + : nUint6 === 62 ? + 43 + : nUint6 === 63 ? + 47 + : + 65; + +}; + +/* Base64 string to array encoding */ + +StringView.bytesToBase64 = function (aBytes) { + + var eqLen = (3 - (aBytes.length % 3)) % 3, sB64Enc = ""; + + for (var nMod3, nLen = aBytes.length, nUint24 = 0, nIdx = 0; nIdx < nLen; nIdx++) { + nMod3 = nIdx % 3; + /* Uncomment the following line in order to split the output in lines 76-character long: */ + /* + if (nIdx > 0 && (nIdx * 4 / 3) % 76 === 0) { sB64Enc += "\r\n"; } + */ + nUint24 |= aBytes[nIdx] << (16 >>> nMod3 & 24); + if (nMod3 === 2 || aBytes.length - nIdx === 1) { + sB64Enc += String.fromCharCode(StringView.uint6ToB64(nUint24 >>> 18 & 63), StringView.uint6ToB64(nUint24 >>> 12 & 63), StringView.uint6ToB64(nUint24 >>> 6 & 63), StringView.uint6ToB64(nUint24 & 63)); + nUint24 = 0; + } + } + + return eqLen === 0 ? + sB64Enc + : + sB64Enc.substring(0, sB64Enc.length - eqLen) + (eqLen === 1 ? "=" : "=="); + + +}; + + +StringView.base64ToBytes = function (sBase64, nBlockBytes) { + + var + sB64Enc = sBase64.replace(/[^A-Za-z0-9\+\/]/g, ""), nInLen = sB64Enc.length, + nOutLen = nBlockBytes ? Math.ceil((nInLen * 3 + 1 >>> 2) / nBlockBytes) * nBlockBytes : nInLen * 3 + 1 >>> 2, aBytes = new Uint8Array(nOutLen); + + for (var nMod3, nMod4, nUint24 = 0, nOutIdx = 0, nInIdx = 0; nInIdx < nInLen; nInIdx++) { + nMod4 = nInIdx & 3; + nUint24 |= StringView.b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << 18 - 6 * nMod4; + if (nMod4 === 3 || nInLen - nInIdx === 1) { + for (nMod3 = 0; nMod3 < 3 && nOutIdx < nOutLen; nMod3++, nOutIdx++) { + aBytes[nOutIdx] = nUint24 >>> (16 >>> nMod3 & 24) & 255; + } + nUint24 = 0; + } + } + + return aBytes; + +}; + +StringView.makeFromBase64 = function (sB64Inpt, sEncoding, nByteOffset, nLength) { + + return new StringView(sEncoding === "UTF-16" || sEncoding === "UTF-32" ? StringView.base64ToBytes(sB64Inpt, sEncoding === "UTF-16" ? 2 : 4).buffer : StringView.base64ToBytes(sB64Inpt), sEncoding, nByteOffset, nLength); + +}; + +/* DEFAULT VALUES */ + +StringView.prototype.encoding = "UTF-8"; /* Default encoding... */ + +/* INSTANCES' METHODS */ + +StringView.prototype.makeIndex = function (nChrLength, nStartFrom) { + + var + + aTarget = this.rawData, nChrEnd, nRawLength = aTarget.length, + nStartIdx = nStartFrom || 0, nIdxEnd = nStartIdx, nStopAtChr = isNaN(nChrLength) ? Infinity : nChrLength; + + if (nChrLength + 1 > aTarget.length) { throw new RangeError("StringView.prototype.makeIndex - The offset can\'t be major than the length of the array - 1."); } + + switch (this.encoding) { + + case "UTF-8": + + var nPart; + + for (nChrEnd = 0; nIdxEnd < nRawLength && nChrEnd < nStopAtChr; nChrEnd++) { + nPart = aTarget[nIdxEnd]; + nIdxEnd += nPart > 251 && nPart < 254 && nIdxEnd + 5 < nRawLength ? 6 + : nPart > 247 && nPart < 252 && nIdxEnd + 4 < nRawLength ? 5 + : nPart > 239 && nPart < 248 && nIdxEnd + 3 < nRawLength ? 4 + : nPart > 223 && nPart < 240 && nIdxEnd + 2 < nRawLength ? 3 + : nPart > 191 && nPart < 224 && nIdxEnd + 1 < nRawLength ? 2 + : 1; + } + + break; + + case "UTF-16": + + for (nChrEnd = nStartIdx; nIdxEnd < nRawLength && nChrEnd < nStopAtChr; nChrEnd++) { + nIdxEnd += aTarget[nIdxEnd] > 0xD7BF /* 55231 */ && nIdxEnd + 1 < aTarget.length ? 2 : 1; + } + + break; + + default: + + nIdxEnd = nChrEnd = isFinite(nChrLength) ? nChrLength : nRawLength - 1; + + } + + if (nChrLength) { return nIdxEnd; } + + return nChrEnd; + +}; + +StringView.prototype.toBase64 = function (bWholeBuffer) { + + return StringView.bytesToBase64( + bWholeBuffer ? + ( + this.bufferView.constructor === Uint8Array ? + this.bufferView + : + new Uint8Array(this.buffer) + ) + : this.rawData.constructor === Uint8Array ? + this.rawData + : + new Uint8Array(this.buffer, this.rawData.byteOffset, this.rawData.length << (this.rawData.constructor === Uint16Array ? 1 : 2)) + ); + +}; + +StringView.prototype.subview = function (nCharOffset /* optional */, nCharLength /* optional */) { + + var + + nChrLen, nCharStart, nStrLen, bVariableLen = this.encoding === "UTF-8" || this.encoding === "UTF-16", + nStartOffset = nCharOffset, nStringLength, nRawLen = this.rawData.length; + + if (nRawLen === 0) { + return new StringView(this.buffer, this.encoding); + } + + nStringLength = bVariableLen ? this.makeIndex() : nRawLen; + nCharStart = nCharOffset ? Math.max((nStringLength + nCharOffset) % nStringLength, 0) : 0; + nStrLen = Number.isInteger(nCharLength) ? Math.max(nCharLength, 0) + nCharStart > nStringLength ? nStringLength - nCharStart : nCharLength : nStringLength; + + if (nCharStart === 0 && nStrLen === nStringLength) { return this; } + + if (bVariableLen) { + nStartOffset = this.makeIndex(nCharStart); + nChrLen = this.makeIndex(nStrLen, nStartOffset) - nStartOffset; + } else { + nStartOffset = nCharStart; + nChrLen = nStrLen - nCharStart; + } + + if (this.encoding === "UTF-16") { + nStartOffset <<= 1; + } else if (this.encoding === "UTF-32") { + nStartOffset <<= 2; + } + + return new StringView(this.buffer, this.encoding, nStartOffset, nChrLen); + +}; + +StringView.prototype.forEachChar = function (fCallback, oThat, nChrOffset, nChrLen) { + + var aSource = this.rawData, nRawEnd, nRawIdx; + + if (this.encoding === "UTF-8" || this.encoding === "UTF-16") { + + var fGetInptChrSize, fGetInptChrCode; + + if (this.encoding === "UTF-8") { + fGetInptChrSize = StringView.getUTF8CharLength; + fGetInptChrCode = StringView.loadUTF8CharCode; + } else if (this.encoding === "UTF-16") { + fGetInptChrSize = StringView.getUTF16CharLength; + fGetInptChrCode = StringView.loadUTF16CharCode; + } + + nRawIdx = isFinite(nChrOffset) ? this.makeIndex(nChrOffset) : 0; + nRawEnd = isFinite(nChrLen) ? this.makeIndex(nChrLen, nRawIdx) : aSource.length; + + for (var nChrCode, nChrIdx = 0; nRawIdx < nRawEnd; nChrIdx++) { + nChrCode = fGetInptChrCode(aSource, nRawIdx); + fCallback.call(oThat || null, nChrCode, nChrIdx, nRawIdx, aSource); + nRawIdx += fGetInptChrSize(nChrCode); + } + + } else { + + nRawIdx = isFinite(nChrOffset) ? nChrOffset : 0; + nRawEnd = isFinite(nChrLen) ? nChrLen + nRawIdx : aSource.length; + + for (nRawIdx; nRawIdx < nRawEnd; nRawIdx++) { + fCallback.call(oThat || null, aSource[nRawIdx], nRawIdx, nRawIdx, aSource); + } + + } + +}; + +StringView.prototype.valueOf = StringView.prototype.toString = function () { + + if (this.encoding !== "UTF-8" && this.encoding !== "UTF-16") { + /* ASCII, UTF-32 or BinaryString to DOMString */ + return String.fromCharCode.apply(null, this.rawData); + } + + var fGetCode, fGetIncr, sView = ""; + + if (this.encoding === "UTF-8") { + fGetIncr = StringView.getUTF8CharLength; + fGetCode = StringView.loadUTF8CharCode; + } else if (this.encoding === "UTF-16") { + fGetIncr = StringView.getUTF16CharLength; + fGetCode = StringView.loadUTF16CharCode; + } + + for (var nChr, nLen = this.rawData.length, nIdx = 0; nIdx < nLen; nIdx += fGetIncr(nChr)) { + nChr = fGetCode(this.rawData, nIdx); + sView += String.fromCharCode(nChr); + } + + return sView; + +}; diff --git a/src/Serialize.cpp b/src/Serialize.cpp index 483b0301..33d8c01d 100644 --- a/src/Serialize.cpp +++ b/src/Serialize.cpp @@ -80,6 +80,13 @@ bool Serialize::storeInternal(Task* task, std::vector& buffer, v8::Handle< v8::String::Utf8Value utf8(value->ToString()); writeInt32(buffer, utf8.length()); buffer.insert(buffer.end(), *utf8, *utf8 + utf8.length()); + } else if (value->IsUint8Array()) { + writeInt32(buffer, kUint8Array); + v8::Handle array = v8::Handle::Cast(value); + char* data = reinterpret_cast(array->GetContents().Data()); + size_t length = array->GetContents().ByteLength(); + writeInt32(buffer, length); + buffer.insert(buffer.end(), data, data + length); } else if (value->IsArray()) { writeInt32(buffer, kArray); v8::Handle array = v8::Handle::Cast(value); @@ -175,6 +182,15 @@ v8::Handle Serialize::loadInternal(Task* task, TaskStub* from, const offset += length; } break; + case kUint8Array: + { + int32_t length = readInt32(buffer, offset); + v8::Handle array = v8::ArrayBuffer::New(task->getIsolate(), length); + std::memcpy(array->GetContents().Data(), &*buffer.begin() + offset, length); + offset += length; + result = v8::Uint8Array::New(array, 0, length); + } + break; case kArray: { int32_t length = readInt32(buffer, offset); diff --git a/src/Serialize.h b/src/Serialize.h index 9bd82d34..eaea7eba 100644 --- a/src/Serialize.h +++ b/src/Serialize.h @@ -38,6 +38,7 @@ private: kNumber, kString, kArray, + kUint8Array, kObject, kFunction, kError, diff --git a/src/Socket.cpp b/src/Socket.cpp index c47e9be0..41b79e4a 100644 --- a/src/Socket.cpp +++ b/src/Socket.cpp @@ -442,11 +442,7 @@ void Socket::onRead(uv_stream_t* stream, ssize_t readSize, const uv_buf_t* buffe char plain[8192]; int result = socket->_tls->readPlain(plain, sizeof(plain)); if (result > 0) { - v8::Local callback = v8::Local::New(socket->_task->getIsolate(), socket->_onRead); - if (!callback.IsEmpty()) { - data = v8::String::NewFromOneByte(socket->_task->getIsolate(), reinterpret_cast(plain), v8::String::kNormalString, result); - callback->Call(callback, 1, &data); - } + socket->notifyDataRead(plain, result); } else if (result == TlsSession::kReadFailed) { socket->reportTlsErrors(); socket->close(); @@ -466,36 +462,52 @@ void Socket::onRead(uv_stream_t* stream, ssize_t readSize, const uv_buf_t* buffe socket->processOutgoingTls(); } } else { - v8::Local callback = v8::Local::New(socket->_task->getIsolate(), socket->_onRead); - if (!callback.IsEmpty()) { - data = v8::String::NewFromOneByte(socket->_task->getIsolate(), reinterpret_cast(buffer->base), v8::String::kNormalString, readSize); - callback->Call(callback, 1, &data); - } + socket->notifyDataRead(buffer->base, readSize); } } } delete[] buffer->base; } +void Socket::notifyDataRead(const char* data, size_t length) { + v8::Local callback = v8::Local::New(_task->getIsolate(), _onRead); + if (!callback.IsEmpty()) { + if (data && length > 0) { + v8::Handle buffer = v8::ArrayBuffer::New(_task->getIsolate(), length); + std::memcpy(buffer->GetContents().Data(), data, length); + v8::Handle array = v8::Uint8Array::New(buffer, 0, length); + + v8::Handle arguments = array; + callback->Call(callback, 1, &arguments); + } + } +} + void Socket::write(const v8::FunctionCallbackInfo& args) { if (Socket* socket = Socket::get(args.Data())) { promiseid_t promise = socket->_task->allocatePromise(); args.GetReturnValue().Set(socket->_task->getPromise(promise)); - v8::Handle value = args[0].As(); - if (!value.IsEmpty() && value->IsString()) { + v8::Handle value = args[0]; + if (!value.IsEmpty() && (value->IsString() || value->IsUint8Array())) { if (socket->_tls) { socket->reportTlsErrors(); int result; int length; - if (value->ContainsOnlyOneByte()) { - length = value->Length(); - std::vector bytes(length); - value->WriteOneByte(bytes.data(), 0, bytes.size(), v8::String::NO_NULL_TERMINATION); - result = socket->_tls->writePlain(reinterpret_cast(bytes.data()), bytes.size()); - } else { - v8::String::Utf8Value utf8(value); - length = utf8.length(); - result = socket->_tls->writePlain(*utf8, utf8.length()); + if (value->IsArrayBufferView()) { + v8::Handle array = v8::Handle::Cast(value); + result = socket->_tls->writePlain(reinterpret_cast(array->Buffer()->GetContents().Data()), array->Buffer()->GetContents().ByteLength()); + } else if (value->IsString()) { + v8::Handle stringValue = v8::Handle::Cast(value); + if (stringValue->ContainsOnlyOneByte()) { + length = stringValue->Length(); + std::vector bytes(length); + stringValue->WriteOneByte(bytes.data(), 0, bytes.size(), v8::String::NO_NULL_TERMINATION); + result = socket->_tls->writePlain(reinterpret_cast(bytes.data()), bytes.size()); + } else { + v8::String::Utf8Value utf8(stringValue); + length = utf8.length(); + result = socket->_tls->writePlain(*utf8, utf8.length()); + } } char buffer[8192]; if (result <= 0 && socket->_tls->getError(buffer, sizeof(buffer))) { @@ -507,18 +519,26 @@ void Socket::write(const v8::FunctionCallbackInfo& args) { } socket->processOutgoingTls(); } else { - v8::String::Utf8Value utf8(value); int length; char* rawBuffer = 0; - if (value->ContainsOnlyOneByte()) { - length = value->Length(); + v8::Handle stringValue = v8::Handle::Cast(value); + if (stringValue->IsArrayBufferView()) { + v8::Handle array = v8::Handle::Cast(value); + length = array->Buffer()->GetContents().ByteLength(); rawBuffer = new char[sizeof(uv_write_t) + length]; - value->WriteOneByte(reinterpret_cast(rawBuffer) + sizeof(uv_write_t), 0, length, v8::String::NO_NULL_TERMINATION); - } else { - v8::String::Utf8Value utf8(value); - length = utf8.length(); - rawBuffer = new char[sizeof(uv_write_t) + length]; - std::memcpy(rawBuffer + sizeof(uv_write_t), *utf8, length); + std::memcpy(rawBuffer + sizeof(uv_write_t), array->Buffer()->GetContents().Data(), length); + } else if (value->IsString()) { + v8::String::Utf8Value utf8(stringValue); + if (stringValue->ContainsOnlyOneByte()) { + length = stringValue->Length(); + rawBuffer = new char[sizeof(uv_write_t) + length]; + stringValue->WriteOneByte(reinterpret_cast(rawBuffer) + sizeof(uv_write_t), 0, length, v8::String::NO_NULL_TERMINATION); + } else { + v8::String::Utf8Value utf8(stringValue); + length = utf8.length(); + rawBuffer = new char[sizeof(uv_write_t) + length]; + std::memcpy(rawBuffer + sizeof(uv_write_t), *utf8, length); + } } uv_write_t* request = reinterpret_cast(rawBuffer); diff --git a/src/Socket.h b/src/Socket.h index 0a5bb3d5..a5759487 100644 --- a/src/Socket.h +++ b/src/Socket.h @@ -74,6 +74,7 @@ private: static void onWrite(uv_write_t* request, int status); static void onRelease(const v8::WeakCallbackInfo& data); + void notifyDataRead(const char* data, size_t length); void processTlsShutdown(promiseid_t promise); static void onTlsShutdown(uv_write_t* request, int status); void shutdownInternal(promiseid_t promise); diff --git a/tests/14-uint8array b/tests/14-uint8array new file mode 100644 index 00000000..613bf647 --- /dev/null +++ b/tests/14-uint8array @@ -0,0 +1,38 @@ +#!/bin/bash + +cat > test.js << EOF +var task = new Task(); +task.onExit = function() { + print("child exited"); +}; +task.activate(); +task.execute({name: "child.js", source: File.readFile("child.js")}).then(async function() { + print("child started"); + var input = new ArrayBuffer(10); + for (var i = 0; i < 10; i++) { + input[i] = i; + } + var test = (await task.getExports()).test; + var output = await test(input); + print("input", input); + print("output", output); + for (var i = 0; i < 10; i++) { + print(output[i]); + if (output[i] != i) { + print("output[" + i + "] == " + output[i]); + exit(1); + } + } + exit(0); +}); +EOF + +cat > child.js << EOF +exports = { + test: function(data) { + return data; + } +} +EOF + +$TILDEFRIENDS test.js diff --git a/tools/run-tests b/tools/run-tests index 8c9c31b5..8ba365ee 100755 --- a/tools/run-tests +++ b/tools/run-tests @@ -42,15 +42,16 @@ def indent(text): passCount = 0 failCount = 0 -for test in selectedTests: +for test in sorted(selectedTests): if os.path.isdir(tmp): shutil.rmtree(tmp) if not os.path.isdir(tmp): os.makedirs(tmp) process = subprocess.Popen(['bash', test], stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=tmp, env=env) + interrupted = False stdout, stderr = process.communicate() - if process.returncode == 0: + if interrupted or process.returncode == 0: if stdout.strip() == "SKIP": print 'SKIPPED', test else: