all files / lib/ Parser.js

100% Statements 89/89
100% Branches 51/51
100% Functions 5/5
100% Lines 89/89
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209                   25×   25× 25×                           10×   10×     10× 10×                                                                                       25×       25× 23× 23×   23× 22× 21×     20× 20×     18× 18×     20× 19× 19×   19× 18× 17×     15× 15×     16× 15×     15×     13×   13× 12× 11×     10× 10×           12× 11×   11× 10×             10×                                  
var EventEmitter = require('events').EventEmitter
  , util = require('util')
  , Message = require('./Message')
 
/**
 * Handles serializing and de-serializing RELP messages
 * Each connection should have a parser
 *
 * @constructor
 */
var Parser = function () {
    Parser.super_.call(this)
 
    this.buffer = new Buffer(0)
    this.currentMessage = null
}
 
util.inherits(Parser, EventEmitter)
module.exports = Parser
 
/**
 * Adds to the existing buffer and consumes it
 *
 * @param buffer
 */
Parser.prototype.consume = function (buffer) {
    var self = this
 
    this.buffer = Buffer.concat([this.buffer, buffer])
 
    /**
     * Buffer consumer that tries to be nice to the event loop
     */
    var consume = function () {
        var result
 
        if (self.currentMessage === null) {
            self.currentMessage = new Message()
        }
 
        try {
            result = self.deserialize(self.buffer, self.currentMessage)
 
        } catch (e) {
            /**
             * Error event that contains a caught exception
             *
             * @event Parser#error
             * @type {Error}
             */
            self.emit('error', e)
            return
        }
 
        self.buffer = self.buffer.slice(result.position)
 
        if (result.complete === false) {
            return
        }
 
        var emitMessage = self.currentMessage
        self.currentMessage = null
 
        if (self.buffer.length > 0) {
            process.nextTick(consume)
        }
 
        /**
         * Message event that contains a parsed Message
         *
         * @event Parser#message
         * @type {Message}
         */
        self.emit('message', emitMessage)
    }
 
    process.nextTick(consume)
}
 
/**
 * Attempts to consume an entire message from the buffer
 * If the buffer does not contain an entire message then `message` will be partial
 *
 * @param {String} buffer The buffer to consume serialized messages from
 * @param {Message} message The message to put deserialized parts onto
 *
 * @returns {{complete: boolean, position: number}} An object describing the state of the message being parsed and the
 *      position in the buffer we got to
 */
Parser.prototype.deserialize = function (buffer, message) {
    /*
    NOTE: Node doesn't provide a good way to read bytes until reaching a specific character, outside of ReadLine.
    For most of this we read utf pessimistically until we get to the message body, since that is the only time we
    know the full size of what we need to read
 
    TODO: In node 10 we are going to have some issues with the backwards compat breaks in how Buffer works
     */
    var parsedPosition = 0,
        nextToken = 0,
        header = ''
 
    if (message.transactionId === void 0) {
        header = buffer.toString('utf8', 0, 10)
        nextToken = header.indexOf(' ', parsedPosition)
 
        if (nextToken === -1 && header.length > 9) {
            throw new Error('Expected transaction id, got something longer than 9 characters')
        } else if (nextToken === 0) {
            throw new Error('Expected transaction id, got a space instead')
        } else if (nextToken < 0) {
            return { complete: false, position: parsedPosition }
        }
 
        var transactionId = parseInt(header.slice(parsedPosition, nextToken))
        if (isNaN(transactionId)) {
            throw new Error('Expected transaction id to be a number, got something else')
        }
 
        message.transactionId = transactionId
        parsedPosition = nextToken + 1
    }
 
    if (message.command === void 0) {
        header = buffer.toString('utf8', parsedPosition, parsedPosition + 33)
        nextToken = header.indexOf(' ')
 
        if (nextToken === -1 && header.length > 32) {
            throw new Error('Expected command, got something longer than 32 characters')
        } else if (nextToken === 0) {
            throw new Error('Expected command, got a space instead')
        } else if (nextToken < 0) {
            return { complete: false, position: parsedPosition }
        }
 
        message.command = header.slice(0, nextToken)
        parsedPosition += nextToken + 1
    }
 
    if (message.bodyLength === void 0) {
        header = buffer.toString('utf8', parsedPosition, parsedPosition + 10)
 
        //No space after body length if it is 0
        if (header[0] === '0') {
            message.bodyLength = 0
            parsedPosition++
 
        } else {
            nextToken = header.indexOf(' ')
 
            if (nextToken > 10 || (nextToken === -1 && header.length > 9)) {
                throw new Error('Expected bodyLength, got something longer than 9 characters')
            } else if (nextToken === 0) {
                throw new Error('Expected bodyLength, got a space instead')
            } else if (nextToken < 0) {
                return { complete: false, position: parsedPosition }
            }
 
            var bodyLength = parseInt(header.slice(0, nextToken))
            if (isNaN(bodyLength)) {
                throw new Error('Expected bodyLength to be a number, got something else')
            }
 
            message.bodyLength = bodyLength
            parsedPosition += nextToken + 1
        }
    }
 
    if (message.body === void 0) {
        var start = parsedPosition + message.bodyLength
 
        if ((buffer.length - parsedPosition) < message.bodyLength + 1) {
            return { complete: false, position: parsedPosition }
        } else if (buffer.toString('utf8', start, start + 1) !== '\n') {
            //TODO: Figure out why this is happening, maybe not recieved it all yet?
            throw new Error('Expected ending newline, got something else')
        }
 
        message.body = buffer.toString('utf8', parsedPosition, parsedPosition + message.bodyLength)
 
        parsedPosition = parsedPosition + message.bodyLength + 1
    }
 
    return { complete: true, position: parsedPosition }
}
 
/**
 * Serializes a RELP message for transmission over the wire
 *
 * @param {Message} message The RELP message to serialize
 *
 * @returns {string} The serialized string, ready to transmit
 */
Parser.prototype.serialize = function (message) {
    //TODO: should probably guard this a bit, id is a number, bodyLength is a number, things aren't too long etc
    var buffer = []
 
    buffer.push(message.transactionId)
    buffer.push(message.command)
 
    if (message.body) {
        buffer.push(Buffer.byteLength(message.body))
        buffer.push(message.body)
    } else {
        buffer.push('0')
    }
 
    return buffer.join(' ') + '\n'
}