michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: michael@0: /** michael@0: * This object contains helpers buffering incoming data & deconstructing it michael@0: * into parcels as well as buffering outgoing data & constructing parcels. michael@0: * For that it maintains two buffers and corresponding uint8 views, indexes. michael@0: * michael@0: * The incoming buffer is a circular buffer where we store incoming data. michael@0: * As soon as a complete parcel is received, it is processed right away, so michael@0: * the buffer only needs to be large enough to hold one parcel. michael@0: * michael@0: * The outgoing buffer is to prepare outgoing parcels. The index is reset michael@0: * every time a parcel is sent. michael@0: */ michael@0: michael@0: let Buf = { michael@0: INT32_MAX: 2147483647, michael@0: UINT8_SIZE: 1, michael@0: UINT16_SIZE: 2, michael@0: UINT32_SIZE: 4, michael@0: PARCEL_SIZE_SIZE: 4, michael@0: PDU_HEX_OCTET_SIZE: 4, michael@0: michael@0: incomingBufferLength: 1024, michael@0: incomingBuffer: null, michael@0: incomingBytes: null, michael@0: incomingWriteIndex: 0, michael@0: incomingReadIndex: 0, michael@0: readIncoming: 0, michael@0: readAvailable: 0, michael@0: currentParcelSize: 0, michael@0: michael@0: outgoingBufferLength: 1024, michael@0: outgoingBuffer: null, michael@0: outgoingBytes: null, michael@0: outgoingIndex: 0, michael@0: outgoingBufferCalSizeQueue: null, michael@0: michael@0: _init: function() { michael@0: this.incomingBuffer = new ArrayBuffer(this.incomingBufferLength); michael@0: this.outgoingBuffer = new ArrayBuffer(this.outgoingBufferLength); michael@0: michael@0: this.incomingBytes = new Uint8Array(this.incomingBuffer); michael@0: this.outgoingBytes = new Uint8Array(this.outgoingBuffer); michael@0: michael@0: // Track where incoming data is read from and written to. michael@0: this.incomingWriteIndex = 0; michael@0: this.incomingReadIndex = 0; michael@0: michael@0: // Leave room for the parcel size for outgoing parcels. michael@0: this.outgoingIndex = this.PARCEL_SIZE_SIZE; michael@0: michael@0: // How many bytes we've read for this parcel so far. michael@0: this.readIncoming = 0; michael@0: michael@0: // How many bytes available as parcel data. michael@0: this.readAvailable = 0; michael@0: michael@0: // Size of the incoming parcel. If this is zero, we're expecting a new michael@0: // parcel. michael@0: this.currentParcelSize = 0; michael@0: michael@0: // Queue for storing outgoing override points michael@0: this.outgoingBufferCalSizeQueue = []; michael@0: }, michael@0: michael@0: /** michael@0: * Mark current outgoingIndex as start point for calculation length of data michael@0: * written to outgoingBuffer. michael@0: * Mark can be nested for here uses queue to remember marks. michael@0: * michael@0: * @param writeFunction michael@0: * Function to write data length into outgoingBuffer, this function is michael@0: * also used to allocate buffer for data length. michael@0: * Raw data size(in Uint8) is provided as parameter calling writeFunction. michael@0: * If raw data size is not in proper unit for writing, user can adjust michael@0: * the length value in writeFunction before writing. michael@0: **/ michael@0: startCalOutgoingSize: function(writeFunction) { michael@0: let sizeInfo = {index: this.outgoingIndex, michael@0: write: writeFunction}; michael@0: michael@0: // Allocate buffer for data lemgtj. michael@0: writeFunction.call(0); michael@0: michael@0: // Get size of data length buffer for it is not counted into data size. michael@0: sizeInfo.size = this.outgoingIndex - sizeInfo.index; michael@0: michael@0: // Enqueue size calculation information. michael@0: this.outgoingBufferCalSizeQueue.push(sizeInfo); michael@0: }, michael@0: michael@0: /** michael@0: * Calculate data length since last mark, and write it into mark position. michael@0: **/ michael@0: stopCalOutgoingSize: function() { michael@0: let sizeInfo = this.outgoingBufferCalSizeQueue.pop(); michael@0: michael@0: // Remember current outgoingIndex. michael@0: let currentOutgoingIndex = this.outgoingIndex; michael@0: // Calculate data length, in uint8. michael@0: let writeSize = this.outgoingIndex - sizeInfo.index - sizeInfo.size; michael@0: michael@0: // Write data length to mark, use same function for allocating buffer to make michael@0: // sure there is no buffer overloading. michael@0: this.outgoingIndex = sizeInfo.index; michael@0: sizeInfo.write(writeSize); michael@0: michael@0: // Restore outgoingIndex. michael@0: this.outgoingIndex = currentOutgoingIndex; michael@0: }, michael@0: michael@0: /** michael@0: * Grow the incoming buffer. michael@0: * michael@0: * @param min_size michael@0: * Minimum new size. The actual new size will be the the smallest michael@0: * power of 2 that's larger than this number. michael@0: */ michael@0: growIncomingBuffer: function(min_size) { michael@0: if (DEBUG) { michael@0: debug("Current buffer of " + this.incomingBufferLength + michael@0: " can't handle incoming " + min_size + " bytes."); michael@0: } michael@0: let oldBytes = this.incomingBytes; michael@0: this.incomingBufferLength = michael@0: 2 << Math.floor(Math.log(min_size)/Math.log(2)); michael@0: if (DEBUG) debug("New incoming buffer size: " + this.incomingBufferLength); michael@0: this.incomingBuffer = new ArrayBuffer(this.incomingBufferLength); michael@0: this.incomingBytes = new Uint8Array(this.incomingBuffer); michael@0: if (this.incomingReadIndex <= this.incomingWriteIndex) { michael@0: // Read and write index are in natural order, so we can just copy michael@0: // the old buffer over to the bigger one without having to worry michael@0: // about the indexes. michael@0: this.incomingBytes.set(oldBytes, 0); michael@0: } else { michael@0: // The write index has wrapped around but the read index hasn't yet. michael@0: // Write whatever the read index has left to read until it would michael@0: // circle around to the beginning of the new buffer, and the rest michael@0: // behind that. michael@0: let head = oldBytes.subarray(this.incomingReadIndex); michael@0: let tail = oldBytes.subarray(0, this.incomingReadIndex); michael@0: this.incomingBytes.set(head, 0); michael@0: this.incomingBytes.set(tail, head.length); michael@0: this.incomingReadIndex = 0; michael@0: this.incomingWriteIndex += head.length; michael@0: } michael@0: if (DEBUG) { michael@0: debug("New incoming buffer size is " + this.incomingBufferLength); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Grow the outgoing buffer. michael@0: * michael@0: * @param min_size michael@0: * Minimum new size. The actual new size will be the the smallest michael@0: * power of 2 that's larger than this number. michael@0: */ michael@0: growOutgoingBuffer: function(min_size) { michael@0: if (DEBUG) { michael@0: debug("Current buffer of " + this.outgoingBufferLength + michael@0: " is too small."); michael@0: } michael@0: let oldBytes = this.outgoingBytes; michael@0: this.outgoingBufferLength = michael@0: 2 << Math.floor(Math.log(min_size)/Math.log(2)); michael@0: this.outgoingBuffer = new ArrayBuffer(this.outgoingBufferLength); michael@0: this.outgoingBytes = new Uint8Array(this.outgoingBuffer); michael@0: this.outgoingBytes.set(oldBytes, 0); michael@0: if (DEBUG) { michael@0: debug("New outgoing buffer size is " + this.outgoingBufferLength); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Functions for reading data from the incoming buffer. michael@0: * michael@0: * These are all little endian, apart from readParcelSize(); michael@0: */ michael@0: michael@0: /** michael@0: * Ensure position specified is readable. michael@0: * michael@0: * @param index michael@0: * Data position in incoming parcel, valid from 0 to michael@0: * currentParcelSize. michael@0: */ michael@0: ensureIncomingAvailable: function(index) { michael@0: if (index >= this.currentParcelSize) { michael@0: throw new Error("Trying to read data beyond the parcel end!"); michael@0: } else if (index < 0) { michael@0: throw new Error("Trying to read data before the parcel begin!"); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Seek in current incoming parcel. michael@0: * michael@0: * @param offset michael@0: * Seek offset in relative to current position. michael@0: */ michael@0: seekIncoming: function(offset) { michael@0: // Translate to 0..currentParcelSize michael@0: let cur = this.currentParcelSize - this.readAvailable; michael@0: michael@0: let newIndex = cur + offset; michael@0: this.ensureIncomingAvailable(newIndex); michael@0: michael@0: // ... incomingReadIndex -->| michael@0: // 0 new cur currentParcelSize michael@0: // |================|=======|====================| michael@0: // |<-- cur -->|<- readAvailable ->| michael@0: // |<-- newIndex -->|<-- new readAvailable -->| michael@0: this.readAvailable = this.currentParcelSize - newIndex; michael@0: michael@0: // Translate back: michael@0: if (this.incomingReadIndex < cur) { michael@0: // The incomingReadIndex is wrapped. michael@0: newIndex += this.incomingBufferLength; michael@0: } michael@0: newIndex += (this.incomingReadIndex - cur); michael@0: newIndex %= this.incomingBufferLength; michael@0: this.incomingReadIndex = newIndex; michael@0: }, michael@0: michael@0: readUint8Unchecked: function() { michael@0: let value = this.incomingBytes[this.incomingReadIndex]; michael@0: this.incomingReadIndex = (this.incomingReadIndex + 1) % michael@0: this.incomingBufferLength; michael@0: return value; michael@0: }, michael@0: michael@0: readUint8: function() { michael@0: // Translate to 0..currentParcelSize michael@0: let cur = this.currentParcelSize - this.readAvailable; michael@0: this.ensureIncomingAvailable(cur); michael@0: michael@0: this.readAvailable--; michael@0: return this.readUint8Unchecked(); michael@0: }, michael@0: michael@0: readUint8Array: function(length) { michael@0: // Translate to 0..currentParcelSize michael@0: let last = this.currentParcelSize - this.readAvailable; michael@0: last += (length - 1); michael@0: this.ensureIncomingAvailable(last); michael@0: michael@0: let array = new Uint8Array(length); michael@0: for (let i = 0; i < length; i++) { michael@0: array[i] = this.readUint8Unchecked(); michael@0: } michael@0: michael@0: this.readAvailable -= length; michael@0: return array; michael@0: }, michael@0: michael@0: readUint16: function() { michael@0: return this.readUint8() | this.readUint8() << 8; michael@0: }, michael@0: michael@0: readInt32: function() { michael@0: return this.readUint8() | this.readUint8() << 8 | michael@0: this.readUint8() << 16 | this.readUint8() << 24; michael@0: }, michael@0: michael@0: readInt32List: function() { michael@0: let length = this.readInt32(); michael@0: let ints = []; michael@0: for (let i = 0; i < length; i++) { michael@0: ints.push(this.readInt32()); michael@0: } michael@0: return ints; michael@0: }, michael@0: michael@0: readString: function() { michael@0: let string_len = this.readInt32(); michael@0: if (string_len < 0 || string_len >= this.INT32_MAX) { michael@0: return null; michael@0: } michael@0: let s = ""; michael@0: for (let i = 0; i < string_len; i++) { michael@0: s += String.fromCharCode(this.readUint16()); michael@0: } michael@0: // Strings are \0\0 delimited, but that isn't part of the length. And michael@0: // if the string length is even, the delimiter is two characters wide. michael@0: // It's insane, I know. michael@0: this.readStringDelimiter(string_len); michael@0: return s; michael@0: }, michael@0: michael@0: readStringList: function() { michael@0: let num_strings = this.readInt32(); michael@0: let strings = []; michael@0: for (let i = 0; i < num_strings; i++) { michael@0: strings.push(this.readString()); michael@0: } michael@0: return strings; michael@0: }, michael@0: michael@0: readStringDelimiter: function(length) { michael@0: let delimiter = this.readUint16(); michael@0: if (!(length & 1)) { michael@0: delimiter |= this.readUint16(); michael@0: } michael@0: if (DEBUG) { michael@0: if (delimiter !== 0) { michael@0: debug("Something's wrong, found string delimiter: " + delimiter); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: readParcelSize: function() { michael@0: return this.readUint8Unchecked() << 24 | michael@0: this.readUint8Unchecked() << 16 | michael@0: this.readUint8Unchecked() << 8 | michael@0: this.readUint8Unchecked(); michael@0: }, michael@0: michael@0: /** michael@0: * Functions for writing data to the outgoing buffer. michael@0: */ michael@0: michael@0: /** michael@0: * Ensure position specified is writable. michael@0: * michael@0: * @param index michael@0: * Data position in outgoing parcel, valid from 0 to michael@0: * outgoingBufferLength. michael@0: */ michael@0: ensureOutgoingAvailable: function(index) { michael@0: if (index >= this.outgoingBufferLength) { michael@0: this.growOutgoingBuffer(index + 1); michael@0: } michael@0: }, michael@0: michael@0: writeUint8: function(value) { michael@0: this.ensureOutgoingAvailable(this.outgoingIndex); michael@0: michael@0: this.outgoingBytes[this.outgoingIndex] = value; michael@0: this.outgoingIndex++; michael@0: }, michael@0: michael@0: writeUint16: function(value) { michael@0: this.writeUint8(value & 0xff); michael@0: this.writeUint8((value >> 8) & 0xff); michael@0: }, michael@0: michael@0: writeInt32: function(value) { michael@0: this.writeUint8(value & 0xff); michael@0: this.writeUint8((value >> 8) & 0xff); michael@0: this.writeUint8((value >> 16) & 0xff); michael@0: this.writeUint8((value >> 24) & 0xff); michael@0: }, michael@0: michael@0: writeString: function(value) { michael@0: if (value == null) { michael@0: this.writeInt32(-1); michael@0: return; michael@0: } michael@0: this.writeInt32(value.length); michael@0: for (let i = 0; i < value.length; i++) { michael@0: this.writeUint16(value.charCodeAt(i)); michael@0: } michael@0: // Strings are \0\0 delimited, but that isn't part of the length. And michael@0: // if the string length is even, the delimiter is two characters wide. michael@0: // It's insane, I know. michael@0: this.writeStringDelimiter(value.length); michael@0: }, michael@0: michael@0: writeStringList: function(strings) { michael@0: this.writeInt32(strings.length); michael@0: for (let i = 0; i < strings.length; i++) { michael@0: this.writeString(strings[i]); michael@0: } michael@0: }, michael@0: michael@0: writeStringDelimiter: function(length) { michael@0: this.writeUint16(0); michael@0: if (!(length & 1)) { michael@0: this.writeUint16(0); michael@0: } michael@0: }, michael@0: michael@0: writeParcelSize: function(value) { michael@0: /** michael@0: * Parcel size will always be the first thing in the parcel byte michael@0: * array, but the last thing written. Store the current index off michael@0: * to a temporary to be reset after we write the size. michael@0: */ michael@0: let currentIndex = this.outgoingIndex; michael@0: this.outgoingIndex = 0; michael@0: this.writeUint8((value >> 24) & 0xff); michael@0: this.writeUint8((value >> 16) & 0xff); michael@0: this.writeUint8((value >> 8) & 0xff); michael@0: this.writeUint8(value & 0xff); michael@0: this.outgoingIndex = currentIndex; michael@0: }, michael@0: michael@0: copyIncomingToOutgoing: function(length) { michael@0: if (!length || (length < 0)) { michael@0: return; michael@0: } michael@0: michael@0: let translatedReadIndexEnd = michael@0: this.currentParcelSize - this.readAvailable + length - 1; michael@0: this.ensureIncomingAvailable(translatedReadIndexEnd); michael@0: michael@0: let translatedWriteIndexEnd = this.outgoingIndex + length - 1; michael@0: this.ensureOutgoingAvailable(translatedWriteIndexEnd); michael@0: michael@0: let newIncomingReadIndex = this.incomingReadIndex + length; michael@0: if (newIncomingReadIndex < this.incomingBufferLength) { michael@0: // Reading won't cause wrapping, go ahead with builtin copy. michael@0: this.outgoingBytes michael@0: .set(this.incomingBytes.subarray(this.incomingReadIndex, michael@0: newIncomingReadIndex), michael@0: this.outgoingIndex); michael@0: } else { michael@0: // Not so lucky. michael@0: newIncomingReadIndex %= this.incomingBufferLength; michael@0: this.outgoingBytes michael@0: .set(this.incomingBytes.subarray(this.incomingReadIndex, michael@0: this.incomingBufferLength), michael@0: this.outgoingIndex); michael@0: if (newIncomingReadIndex) { michael@0: let firstPartLength = this.incomingBufferLength - this.incomingReadIndex; michael@0: this.outgoingBytes.set(this.incomingBytes.subarray(0, newIncomingReadIndex), michael@0: this.outgoingIndex + firstPartLength); michael@0: } michael@0: } michael@0: michael@0: this.incomingReadIndex = newIncomingReadIndex; michael@0: this.readAvailable -= length; michael@0: this.outgoingIndex += length; michael@0: }, michael@0: michael@0: /** michael@0: * Parcel management michael@0: */ michael@0: michael@0: /** michael@0: * Write incoming data to the circular buffer. michael@0: * michael@0: * @param incoming michael@0: * Uint8Array containing the incoming data. michael@0: */ michael@0: writeToIncoming: function(incoming) { michael@0: // We don't have to worry about the head catching the tail since michael@0: // we process any backlog in parcels immediately, before writing michael@0: // new data to the buffer. So the only edge case we need to handle michael@0: // is when the incoming data is larger than the buffer size. michael@0: let minMustAvailableSize = incoming.length + this.readIncoming; michael@0: if (minMustAvailableSize > this.incomingBufferLength) { michael@0: this.growIncomingBuffer(minMustAvailableSize); michael@0: } michael@0: michael@0: // We can let the typed arrays do the copying if the incoming data won't michael@0: // wrap around the edges of the circular buffer. michael@0: let remaining = this.incomingBufferLength - this.incomingWriteIndex; michael@0: if (remaining >= incoming.length) { michael@0: this.incomingBytes.set(incoming, this.incomingWriteIndex); michael@0: } else { michael@0: // The incoming data would wrap around it. michael@0: let head = incoming.subarray(0, remaining); michael@0: let tail = incoming.subarray(remaining); michael@0: this.incomingBytes.set(head, this.incomingWriteIndex); michael@0: this.incomingBytes.set(tail, 0); michael@0: } michael@0: this.incomingWriteIndex = (this.incomingWriteIndex + incoming.length) % michael@0: this.incomingBufferLength; michael@0: }, michael@0: michael@0: /** michael@0: * Process incoming data. michael@0: * michael@0: * @param incoming michael@0: * Uint8Array containing the incoming data. michael@0: */ michael@0: processIncoming: function(incoming) { michael@0: if (DEBUG) { michael@0: debug("Received " + incoming.length + " bytes."); michael@0: debug("Already read " + this.readIncoming); michael@0: } michael@0: michael@0: this.writeToIncoming(incoming); michael@0: this.readIncoming += incoming.length; michael@0: while (true) { michael@0: if (!this.currentParcelSize) { michael@0: // We're expecting a new parcel. michael@0: if (this.readIncoming < this.PARCEL_SIZE_SIZE) { michael@0: // We don't know how big the next parcel is going to be, need more michael@0: // data. michael@0: if (DEBUG) debug("Next parcel size unknown, going to sleep."); michael@0: return; michael@0: } michael@0: this.currentParcelSize = this.readParcelSize(); michael@0: if (DEBUG) { michael@0: debug("New incoming parcel of size " + this.currentParcelSize); michael@0: } michael@0: // The size itself is not included in the size. michael@0: this.readIncoming -= this.PARCEL_SIZE_SIZE; michael@0: } michael@0: michael@0: if (this.readIncoming < this.currentParcelSize) { michael@0: // We haven't read enough yet in order to be able to process a parcel. michael@0: if (DEBUG) debug("Read " + this.readIncoming + ", but parcel size is " michael@0: + this.currentParcelSize + ". Going to sleep."); michael@0: return; michael@0: } michael@0: michael@0: // Alright, we have enough data to process at least one whole parcel. michael@0: // Let's do that. michael@0: let expectedAfterIndex = (this.incomingReadIndex + this.currentParcelSize) michael@0: % this.incomingBufferLength; michael@0: michael@0: if (DEBUG) { michael@0: let parcel; michael@0: if (expectedAfterIndex < this.incomingReadIndex) { michael@0: let head = this.incomingBytes.subarray(this.incomingReadIndex); michael@0: let tail = this.incomingBytes.subarray(0, expectedAfterIndex); michael@0: parcel = Array.slice(head).concat(Array.slice(tail)); michael@0: } else { michael@0: parcel = Array.slice(this.incomingBytes.subarray( michael@0: this.incomingReadIndex, expectedAfterIndex)); michael@0: } michael@0: debug("Parcel (size " + this.currentParcelSize + "): " + parcel); michael@0: } michael@0: michael@0: if (DEBUG) debug("We have at least one complete parcel."); michael@0: try { michael@0: this.readAvailable = this.currentParcelSize; michael@0: this.processParcel(); michael@0: } catch (ex) { michael@0: if (DEBUG) debug("Parcel handling threw " + ex + "\n" + ex.stack); michael@0: } michael@0: michael@0: // Ensure that the whole parcel was consumed. michael@0: if (this.incomingReadIndex != expectedAfterIndex) { michael@0: if (DEBUG) { michael@0: debug("Parcel handler didn't consume whole parcel, " + michael@0: Math.abs(expectedAfterIndex - this.incomingReadIndex) + michael@0: " bytes left over"); michael@0: } michael@0: this.incomingReadIndex = expectedAfterIndex; michael@0: } michael@0: this.readIncoming -= this.currentParcelSize; michael@0: this.readAvailable = 0; michael@0: this.currentParcelSize = 0; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Communicate with the IPC thread. michael@0: */ michael@0: sendParcel: function() { michael@0: // Compute the size of the parcel and write it to the front of the parcel michael@0: // where we left room for it. Note that he parcel size does not include michael@0: // the size itself. michael@0: let parcelSize = this.outgoingIndex - this.PARCEL_SIZE_SIZE; michael@0: this.writeParcelSize(parcelSize); michael@0: michael@0: // This assumes that postRILMessage will make a copy of the ArrayBufferView michael@0: // right away! michael@0: let parcel = this.outgoingBytes.subarray(0, this.outgoingIndex); michael@0: if (DEBUG) debug("Outgoing parcel: " + Array.slice(parcel)); michael@0: this.onSendParcel(parcel); michael@0: this.outgoingIndex = this.PARCEL_SIZE_SIZE; michael@0: }, michael@0: michael@0: getCurrentParcelSize: function() { michael@0: return this.currentParcelSize; michael@0: }, michael@0: michael@0: getReadAvailable: function() { michael@0: return this.readAvailable; michael@0: } michael@0: michael@0: /** michael@0: * Process one parcel. michael@0: * michael@0: * |processParcel| is an implementation provided incoming parcel processing michael@0: * function invoked when we have received a complete parcel. Implementation michael@0: * may call multiple read functions to extract data from the incoming buffer. michael@0: */ michael@0: //processParcel: function() { michael@0: // let something = this.readInt32(); michael@0: // ... michael@0: //}, michael@0: michael@0: /** michael@0: * Write raw data out to underlying channel. michael@0: * michael@0: * |onSendParcel| is an implementation provided stream output function michael@0: * invoked when we're really going to write something out. We assume the michael@0: * data are completely copied to some output buffer in this call and may michael@0: * be destroyed when it's done. michael@0: * michael@0: * @param parcel michael@0: * An array of numeric octet data. michael@0: */ michael@0: //onSendParcel: function(parcel) { michael@0: // ... michael@0: //} michael@0: }; michael@0: michael@0: module.exports = { Buf: Buf };