123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581 |
- 'use strict'
- var StringDecoder = require('string_decoder').StringDecoder
- var decoder = new StringDecoder()
- var ReplyError = require('./replyError')
- var ParserError = require('./parserError')
- var bufferPool = bufferAlloc(32 * 1024)
- var bufferOffset = 0
- var interval = null
- var counter = 0
- var notDecreased = 0
- var isModern = typeof Buffer.allocUnsafe === 'function'
- /**
- * For backwards compatibility
- * @param len
- * @returns {Buffer}
- */
- function bufferAlloc (len) {
- return isModern ? Buffer.allocUnsafe(len) : new Buffer(len)
- }
- /**
- * Used for lengths and numbers only, faster perf on arrays / bulks
- * @param parser
- * @returns {*}
- */
- function parseSimpleNumbers (parser) {
- var offset = parser.offset
- var length = parser.buffer.length - 1
- var number = 0
- var sign = 1
- if (parser.buffer[offset] === 45) {
- sign = -1
- offset++
- }
- while (offset < length) {
- var c1 = parser.buffer[offset++]
- if (c1 === 13) { // \r\n
- parser.offset = offset + 1
- return sign * number
- }
- number = (number * 10) + (c1 - 48)
- }
- }
- /**
- * Used for integer numbers in case of the returnNumbers option
- *
- * The maximimum possible integer to use is: Math.floor(Number.MAX_SAFE_INTEGER / 10)
- * Staying in a SMI Math.floor((Math.pow(2, 32) / 10) - 1) is even more efficient though
- *
- * @param parser
- * @returns {*}
- */
- function parseStringNumbers (parser) {
- var offset = parser.offset
- var length = parser.buffer.length - 1
- var number = 0
- var res = ''
- if (parser.buffer[offset] === 45) {
- res += '-'
- offset++
- }
- while (offset < length) {
- var c1 = parser.buffer[offset++]
- if (c1 === 13) { // \r\n
- parser.offset = offset + 1
- if (number !== 0) {
- res += number
- }
- return res
- } else if (number > 429496728) {
- res += (number * 10) + (c1 - 48)
- number = 0
- } else if (c1 === 48 && number === 0) {
- res += 0
- } else {
- number = (number * 10) + (c1 - 48)
- }
- }
- }
- /**
- * Returns a string or buffer of the provided offset start and
- * end ranges. Checks `optionReturnBuffers`.
- *
- * If returnBuffers is active, all return values are returned as buffers besides numbers and errors
- *
- * @param parser
- * @param start
- * @param end
- * @returns {*}
- */
- function convertBufferRange (parser, start, end) {
- parser.offset = end + 2
- if (parser.optionReturnBuffers === true) {
- return parser.buffer.slice(start, end)
- }
- return parser.buffer.toString('utf-8', start, end)
- }
- /**
- * Parse a '+' redis simple string response but forward the offsets
- * onto convertBufferRange to generate a string.
- * @param parser
- * @returns {*}
- */
- function parseSimpleString (parser) {
- var start = parser.offset
- var offset = start
- var buffer = parser.buffer
- var length = buffer.length - 1
- while (offset < length) {
- if (buffer[offset++] === 13) { // \r\n
- return convertBufferRange(parser, start, offset - 1)
- }
- }
- }
- /**
- * Returns the string length via parseSimpleNumbers
- * @param parser
- * @returns {*}
- */
- function parseLength (parser) {
- var string = parseSimpleNumbers(parser)
- if (string !== undefined) {
- return string
- }
- }
- /**
- * Parse a ':' redis integer response
- *
- * If stringNumbers is activated the parser always returns numbers as string
- * This is important for big numbers (number > Math.pow(2, 53)) as js numbers
- * are 64bit floating point numbers with reduced precision
- *
- * @param parser
- * @returns {*}
- */
- function parseInteger (parser) {
- if (parser.optionStringNumbers) {
- return parseStringNumbers(parser)
- }
- return parseSimpleNumbers(parser)
- }
- /**
- * Parse a '$' redis bulk string response
- * @param parser
- * @returns {*}
- */
- function parseBulkString (parser) {
- var length = parseLength(parser)
- if (length === undefined) {
- return
- }
- if (length === -1) {
- return null
- }
- var offsetEnd = parser.offset + length
- if (offsetEnd + 2 > parser.buffer.length) {
- parser.bigStrSize = offsetEnd + 2
- parser.bigOffset = parser.offset
- parser.totalChunkSize = parser.buffer.length
- parser.bufferCache.push(parser.buffer)
- return
- }
- return convertBufferRange(parser, parser.offset, offsetEnd)
- }
- /**
- * Parse a '-' redis error response
- * @param parser
- * @returns {Error}
- */
- function parseError (parser) {
- var string = parseSimpleString(parser)
- if (string !== undefined) {
- if (parser.optionReturnBuffers === true) {
- string = string.toString()
- }
- return new ReplyError(string)
- }
- }
- /**
- * Parsing error handler, resets parser buffer
- * @param parser
- * @param error
- */
- function handleError (parser, error) {
- parser.buffer = null
- parser.returnFatalError(error)
- }
- /**
- * Parse a '*' redis array response
- * @param parser
- * @returns {*}
- */
- function parseArray (parser) {
- var length = parseLength(parser)
- if (length === undefined) {
- return
- }
- if (length === -1) {
- return null
- }
- var responses = new Array(length)
- return parseArrayElements(parser, responses, 0)
- }
- /**
- * Push a partly parsed array to the stack
- *
- * @param parser
- * @param elem
- * @param i
- * @returns {undefined}
- */
- function pushArrayCache (parser, elem, pos) {
- parser.arrayCache.push(elem)
- parser.arrayPos.push(pos)
- }
- /**
- * Parse chunked redis array response
- * @param parser
- * @returns {*}
- */
- function parseArrayChunks (parser) {
- var tmp = parser.arrayCache.pop()
- var pos = parser.arrayPos.pop()
- if (parser.arrayCache.length) {
- var res = parseArrayChunks(parser)
- if (!res) {
- pushArrayCache(parser, tmp, pos)
- return
- }
- tmp[pos++] = res
- }
- return parseArrayElements(parser, tmp, pos)
- }
- /**
- * Parse redis array response elements
- * @param parser
- * @param responses
- * @param i
- * @returns {*}
- */
- function parseArrayElements (parser, responses, i) {
- var bufferLength = parser.buffer.length
- while (i < responses.length) {
- var offset = parser.offset
- if (parser.offset >= bufferLength) {
- pushArrayCache(parser, responses, i)
- return
- }
- var response = parseType(parser, parser.buffer[parser.offset++])
- if (response === undefined) {
- if (!parser.arrayCache.length) {
- parser.offset = offset
- }
- pushArrayCache(parser, responses, i)
- return
- }
- responses[i] = response
- i++
- }
- return responses
- }
- /**
- * Called the appropriate parser for the specified type.
- * @param parser
- * @param type
- * @returns {*}
- */
- function parseType (parser, type) {
- switch (type) {
- case 36: // $
- return parseBulkString(parser)
- case 58: // :
- return parseInteger(parser)
- case 43: // +
- return parseSimpleString(parser)
- case 42: // *
- return parseArray(parser)
- case 45: // -
- return parseError(parser)
- default:
- return handleError(parser, new ParserError(
- 'Protocol error, got ' + JSON.stringify(String.fromCharCode(type)) + ' as reply type byte',
- JSON.stringify(parser.buffer),
- parser.offset
- ))
- }
- }
- // All allowed options including their typeof value
- var optionTypes = {
- returnError: 'function',
- returnFatalError: 'function',
- returnReply: 'function',
- returnBuffers: 'boolean',
- stringNumbers: 'boolean',
- name: 'string'
- }
- /**
- * Javascript Redis Parser
- * @param options
- * @constructor
- */
- function JavascriptRedisParser (options) {
- if (!(this instanceof JavascriptRedisParser)) {
- return new JavascriptRedisParser(options)
- }
- if (!options || !options.returnError || !options.returnReply) {
- throw new TypeError('Please provide all return functions while initiating the parser')
- }
- for (var key in options) {
- // eslint-disable-next-line valid-typeof
- if (optionTypes.hasOwnProperty(key) && typeof options[key] !== optionTypes[key]) {
- throw new TypeError('The options argument contains the property "' + key + '" that is either unknown or of a wrong type')
- }
- }
- if (options.name === 'hiredis') {
- /* istanbul ignore next: hiredis is only supported for legacy usage */
- try {
- var Hiredis = require('./hiredis')
- console.error(new TypeError('Using hiredis is discouraged. Please use the faster JS parser by removing the name option.').stack.replace('Error', 'Warning'))
- return new Hiredis(options)
- } catch (e) {
- console.error(new TypeError('Hiredis is not installed. Please remove the `name` option. The (faster) JS parser is used instead.').stack.replace('Error', 'Warning'))
- }
- }
- this.optionReturnBuffers = !!options.returnBuffers
- this.optionStringNumbers = !!options.stringNumbers
- this.returnError = options.returnError
- this.returnFatalError = options.returnFatalError || options.returnError
- this.returnReply = options.returnReply
- this.name = 'javascript'
- this.reset()
- }
- /**
- * Reset the parser values to the initial state
- *
- * @returns {undefined}
- */
- JavascriptRedisParser.prototype.reset = function () {
- this.offset = 0
- this.buffer = null
- this.bigStrSize = 0
- this.bigOffset = 0
- this.totalChunkSize = 0
- this.bufferCache = []
- this.arrayCache = []
- this.arrayPos = []
- }
- /**
- * Set the returnBuffers option
- *
- * @param returnBuffers
- * @returns {undefined}
- */
- JavascriptRedisParser.prototype.setReturnBuffers = function (returnBuffers) {
- if (typeof returnBuffers !== 'boolean') {
- throw new TypeError('The returnBuffers argument has to be a boolean')
- }
- this.optionReturnBuffers = returnBuffers
- }
- /**
- * Set the stringNumbers option
- *
- * @param stringNumbers
- * @returns {undefined}
- */
- JavascriptRedisParser.prototype.setStringNumbers = function (stringNumbers) {
- if (typeof stringNumbers !== 'boolean') {
- throw new TypeError('The stringNumbers argument has to be a boolean')
- }
- this.optionStringNumbers = stringNumbers
- }
- /**
- * Decrease the bufferPool size over time
- * @returns {undefined}
- */
- function decreaseBufferPool () {
- if (bufferPool.length > 50 * 1024) {
- // Balance between increasing and decreasing the bufferPool
- if (counter === 1 || notDecreased > counter * 2) {
- // Decrease the bufferPool by 10% by removing the first 10% of the current pool
- var sliceLength = Math.floor(bufferPool.length / 10)
- if (bufferOffset <= sliceLength) {
- bufferOffset = 0
- } else {
- bufferOffset -= sliceLength
- }
- bufferPool = bufferPool.slice(sliceLength, bufferPool.length)
- } else {
- notDecreased++
- counter--
- }
- } else {
- clearInterval(interval)
- counter = 0
- notDecreased = 0
- interval = null
- }
- }
- /**
- * Check if the requested size fits in the current bufferPool.
- * If it does not, reset and increase the bufferPool accordingly.
- *
- * @param length
- * @returns {undefined}
- */
- function resizeBuffer (length) {
- if (bufferPool.length < length + bufferOffset) {
- var multiplier = length > 1024 * 1024 * 75 ? 2 : 3
- if (bufferOffset > 1024 * 1024 * 111) {
- bufferOffset = 1024 * 1024 * 50
- }
- bufferPool = bufferAlloc(length * multiplier + bufferOffset)
- bufferOffset = 0
- counter++
- if (interval === null) {
- interval = setInterval(decreaseBufferPool, 50)
- }
- }
- }
- /**
- * Concat a bulk string containing multiple chunks
- *
- * Notes:
- * 1) The first chunk might contain the whole bulk string including the \r
- * 2) We are only safe to fully add up elements that are neither the first nor any of the last two elements
- *
- * @param parser
- * @returns {String}
- */
- function concatBulkString (parser) {
- var list = parser.bufferCache
- var chunks = list.length
- var offset = parser.bigStrSize - parser.totalChunkSize
- parser.offset = offset
- if (offset <= 2) {
- if (chunks === 2) {
- return list[0].toString('utf8', parser.bigOffset, list[0].length + offset - 2)
- }
- chunks--
- offset = list[list.length - 2].length + offset
- }
- var res = decoder.write(list[0].slice(parser.bigOffset))
- for (var i = 1; i < chunks - 1; i++) {
- res += decoder.write(list[i])
- }
- res += decoder.end(list[i].slice(0, offset - 2))
- return res
- }
- /**
- * Concat the collected chunks from parser.bufferCache.
- *
- * Increases the bufferPool size beforehand if necessary.
- *
- * @param parser
- * @returns {Buffer}
- */
- function concatBulkBuffer (parser) {
- var list = parser.bufferCache
- var chunks = list.length
- var length = parser.bigStrSize - parser.bigOffset - 2
- var offset = parser.bigStrSize - parser.totalChunkSize
- parser.offset = offset
- if (offset <= 2) {
- if (chunks === 2) {
- return list[0].slice(parser.bigOffset, list[0].length + offset - 2)
- }
- chunks--
- offset = list[list.length - 2].length + offset
- }
- resizeBuffer(length)
- var start = bufferOffset
- list[0].copy(bufferPool, start, parser.bigOffset, list[0].length)
- bufferOffset += list[0].length - parser.bigOffset
- for (var i = 1; i < chunks - 1; i++) {
- list[i].copy(bufferPool, bufferOffset)
- bufferOffset += list[i].length
- }
- list[i].copy(bufferPool, bufferOffset, 0, offset - 2)
- bufferOffset += offset - 2
- return bufferPool.slice(start, bufferOffset)
- }
- /**
- * Parse the redis buffer
- * @param buffer
- * @returns {undefined}
- */
- JavascriptRedisParser.prototype.execute = function execute (buffer) {
- if (this.buffer === null) {
- this.buffer = buffer
- this.offset = 0
- } else if (this.bigStrSize === 0) {
- var oldLength = this.buffer.length
- var remainingLength = oldLength - this.offset
- var newBuffer = bufferAlloc(remainingLength + buffer.length)
- this.buffer.copy(newBuffer, 0, this.offset, oldLength)
- buffer.copy(newBuffer, remainingLength, 0, buffer.length)
- this.buffer = newBuffer
- this.offset = 0
- if (this.arrayCache.length) {
- var arr = parseArrayChunks(this)
- if (!arr) {
- return
- }
- this.returnReply(arr)
- }
- } else if (this.totalChunkSize + buffer.length >= this.bigStrSize) {
- this.bufferCache.push(buffer)
- var tmp = this.optionReturnBuffers ? concatBulkBuffer(this) : concatBulkString(this)
- this.bigStrSize = 0
- this.bufferCache = []
- this.buffer = buffer
- if (this.arrayCache.length) {
- this.arrayCache[0][this.arrayPos[0]++] = tmp
- tmp = parseArrayChunks(this)
- if (!tmp) {
- return
- }
- }
- this.returnReply(tmp)
- } else {
- this.bufferCache.push(buffer)
- this.totalChunkSize += buffer.length
- return
- }
- while (this.offset < this.buffer.length) {
- var offset = this.offset
- var type = this.buffer[this.offset++]
- var response = parseType(this, type)
- if (response === undefined) {
- if (!this.arrayCache.length) {
- this.offset = offset
- }
- return
- }
- if (type === 45) {
- this.returnError(response)
- } else {
- this.returnReply(response)
- }
- }
- this.buffer = null
- }
- module.exports = JavascriptRedisParser
|