parser.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581
  1. 'use strict'
  2. var StringDecoder = require('string_decoder').StringDecoder
  3. var decoder = new StringDecoder()
  4. var ReplyError = require('./replyError')
  5. var ParserError = require('./parserError')
  6. var bufferPool = bufferAlloc(32 * 1024)
  7. var bufferOffset = 0
  8. var interval = null
  9. var counter = 0
  10. var notDecreased = 0
  11. var isModern = typeof Buffer.allocUnsafe === 'function'
  12. /**
  13. * For backwards compatibility
  14. * @param len
  15. * @returns {Buffer}
  16. */
  17. function bufferAlloc (len) {
  18. return isModern ? Buffer.allocUnsafe(len) : new Buffer(len)
  19. }
  20. /**
  21. * Used for lengths and numbers only, faster perf on arrays / bulks
  22. * @param parser
  23. * @returns {*}
  24. */
  25. function parseSimpleNumbers (parser) {
  26. var offset = parser.offset
  27. var length = parser.buffer.length - 1
  28. var number = 0
  29. var sign = 1
  30. if (parser.buffer[offset] === 45) {
  31. sign = -1
  32. offset++
  33. }
  34. while (offset < length) {
  35. var c1 = parser.buffer[offset++]
  36. if (c1 === 13) { // \r\n
  37. parser.offset = offset + 1
  38. return sign * number
  39. }
  40. number = (number * 10) + (c1 - 48)
  41. }
  42. }
  43. /**
  44. * Used for integer numbers in case of the returnNumbers option
  45. *
  46. * The maximimum possible integer to use is: Math.floor(Number.MAX_SAFE_INTEGER / 10)
  47. * Staying in a SMI Math.floor((Math.pow(2, 32) / 10) - 1) is even more efficient though
  48. *
  49. * @param parser
  50. * @returns {*}
  51. */
  52. function parseStringNumbers (parser) {
  53. var offset = parser.offset
  54. var length = parser.buffer.length - 1
  55. var number = 0
  56. var res = ''
  57. if (parser.buffer[offset] === 45) {
  58. res += '-'
  59. offset++
  60. }
  61. while (offset < length) {
  62. var c1 = parser.buffer[offset++]
  63. if (c1 === 13) { // \r\n
  64. parser.offset = offset + 1
  65. if (number !== 0) {
  66. res += number
  67. }
  68. return res
  69. } else if (number > 429496728) {
  70. res += (number * 10) + (c1 - 48)
  71. number = 0
  72. } else if (c1 === 48 && number === 0) {
  73. res += 0
  74. } else {
  75. number = (number * 10) + (c1 - 48)
  76. }
  77. }
  78. }
  79. /**
  80. * Returns a string or buffer of the provided offset start and
  81. * end ranges. Checks `optionReturnBuffers`.
  82. *
  83. * If returnBuffers is active, all return values are returned as buffers besides numbers and errors
  84. *
  85. * @param parser
  86. * @param start
  87. * @param end
  88. * @returns {*}
  89. */
  90. function convertBufferRange (parser, start, end) {
  91. parser.offset = end + 2
  92. if (parser.optionReturnBuffers === true) {
  93. return parser.buffer.slice(start, end)
  94. }
  95. return parser.buffer.toString('utf-8', start, end)
  96. }
  97. /**
  98. * Parse a '+' redis simple string response but forward the offsets
  99. * onto convertBufferRange to generate a string.
  100. * @param parser
  101. * @returns {*}
  102. */
  103. function parseSimpleString (parser) {
  104. var start = parser.offset
  105. var offset = start
  106. var buffer = parser.buffer
  107. var length = buffer.length - 1
  108. while (offset < length) {
  109. if (buffer[offset++] === 13) { // \r\n
  110. return convertBufferRange(parser, start, offset - 1)
  111. }
  112. }
  113. }
  114. /**
  115. * Returns the string length via parseSimpleNumbers
  116. * @param parser
  117. * @returns {*}
  118. */
  119. function parseLength (parser) {
  120. var string = parseSimpleNumbers(parser)
  121. if (string !== undefined) {
  122. return string
  123. }
  124. }
  125. /**
  126. * Parse a ':' redis integer response
  127. *
  128. * If stringNumbers is activated the parser always returns numbers as string
  129. * This is important for big numbers (number > Math.pow(2, 53)) as js numbers
  130. * are 64bit floating point numbers with reduced precision
  131. *
  132. * @param parser
  133. * @returns {*}
  134. */
  135. function parseInteger (parser) {
  136. if (parser.optionStringNumbers) {
  137. return parseStringNumbers(parser)
  138. }
  139. return parseSimpleNumbers(parser)
  140. }
  141. /**
  142. * Parse a '$' redis bulk string response
  143. * @param parser
  144. * @returns {*}
  145. */
  146. function parseBulkString (parser) {
  147. var length = parseLength(parser)
  148. if (length === undefined) {
  149. return
  150. }
  151. if (length === -1) {
  152. return null
  153. }
  154. var offsetEnd = parser.offset + length
  155. if (offsetEnd + 2 > parser.buffer.length) {
  156. parser.bigStrSize = offsetEnd + 2
  157. parser.bigOffset = parser.offset
  158. parser.totalChunkSize = parser.buffer.length
  159. parser.bufferCache.push(parser.buffer)
  160. return
  161. }
  162. return convertBufferRange(parser, parser.offset, offsetEnd)
  163. }
  164. /**
  165. * Parse a '-' redis error response
  166. * @param parser
  167. * @returns {Error}
  168. */
  169. function parseError (parser) {
  170. var string = parseSimpleString(parser)
  171. if (string !== undefined) {
  172. if (parser.optionReturnBuffers === true) {
  173. string = string.toString()
  174. }
  175. return new ReplyError(string)
  176. }
  177. }
  178. /**
  179. * Parsing error handler, resets parser buffer
  180. * @param parser
  181. * @param error
  182. */
  183. function handleError (parser, error) {
  184. parser.buffer = null
  185. parser.returnFatalError(error)
  186. }
  187. /**
  188. * Parse a '*' redis array response
  189. * @param parser
  190. * @returns {*}
  191. */
  192. function parseArray (parser) {
  193. var length = parseLength(parser)
  194. if (length === undefined) {
  195. return
  196. }
  197. if (length === -1) {
  198. return null
  199. }
  200. var responses = new Array(length)
  201. return parseArrayElements(parser, responses, 0)
  202. }
  203. /**
  204. * Push a partly parsed array to the stack
  205. *
  206. * @param parser
  207. * @param elem
  208. * @param i
  209. * @returns {undefined}
  210. */
  211. function pushArrayCache (parser, elem, pos) {
  212. parser.arrayCache.push(elem)
  213. parser.arrayPos.push(pos)
  214. }
  215. /**
  216. * Parse chunked redis array response
  217. * @param parser
  218. * @returns {*}
  219. */
  220. function parseArrayChunks (parser) {
  221. var tmp = parser.arrayCache.pop()
  222. var pos = parser.arrayPos.pop()
  223. if (parser.arrayCache.length) {
  224. var res = parseArrayChunks(parser)
  225. if (!res) {
  226. pushArrayCache(parser, tmp, pos)
  227. return
  228. }
  229. tmp[pos++] = res
  230. }
  231. return parseArrayElements(parser, tmp, pos)
  232. }
  233. /**
  234. * Parse redis array response elements
  235. * @param parser
  236. * @param responses
  237. * @param i
  238. * @returns {*}
  239. */
  240. function parseArrayElements (parser, responses, i) {
  241. var bufferLength = parser.buffer.length
  242. while (i < responses.length) {
  243. var offset = parser.offset
  244. if (parser.offset >= bufferLength) {
  245. pushArrayCache(parser, responses, i)
  246. return
  247. }
  248. var response = parseType(parser, parser.buffer[parser.offset++])
  249. if (response === undefined) {
  250. if (!parser.arrayCache.length) {
  251. parser.offset = offset
  252. }
  253. pushArrayCache(parser, responses, i)
  254. return
  255. }
  256. responses[i] = response
  257. i++
  258. }
  259. return responses
  260. }
  261. /**
  262. * Called the appropriate parser for the specified type.
  263. * @param parser
  264. * @param type
  265. * @returns {*}
  266. */
  267. function parseType (parser, type) {
  268. switch (type) {
  269. case 36: // $
  270. return parseBulkString(parser)
  271. case 58: // :
  272. return parseInteger(parser)
  273. case 43: // +
  274. return parseSimpleString(parser)
  275. case 42: // *
  276. return parseArray(parser)
  277. case 45: // -
  278. return parseError(parser)
  279. default:
  280. return handleError(parser, new ParserError(
  281. 'Protocol error, got ' + JSON.stringify(String.fromCharCode(type)) + ' as reply type byte',
  282. JSON.stringify(parser.buffer),
  283. parser.offset
  284. ))
  285. }
  286. }
  287. // All allowed options including their typeof value
  288. var optionTypes = {
  289. returnError: 'function',
  290. returnFatalError: 'function',
  291. returnReply: 'function',
  292. returnBuffers: 'boolean',
  293. stringNumbers: 'boolean',
  294. name: 'string'
  295. }
  296. /**
  297. * Javascript Redis Parser
  298. * @param options
  299. * @constructor
  300. */
  301. function JavascriptRedisParser (options) {
  302. if (!(this instanceof JavascriptRedisParser)) {
  303. return new JavascriptRedisParser(options)
  304. }
  305. if (!options || !options.returnError || !options.returnReply) {
  306. throw new TypeError('Please provide all return functions while initiating the parser')
  307. }
  308. for (var key in options) {
  309. // eslint-disable-next-line valid-typeof
  310. if (optionTypes.hasOwnProperty(key) && typeof options[key] !== optionTypes[key]) {
  311. throw new TypeError('The options argument contains the property "' + key + '" that is either unknown or of a wrong type')
  312. }
  313. }
  314. if (options.name === 'hiredis') {
  315. /* istanbul ignore next: hiredis is only supported for legacy usage */
  316. try {
  317. var Hiredis = require('./hiredis')
  318. console.error(new TypeError('Using hiredis is discouraged. Please use the faster JS parser by removing the name option.').stack.replace('Error', 'Warning'))
  319. return new Hiredis(options)
  320. } catch (e) {
  321. console.error(new TypeError('Hiredis is not installed. Please remove the `name` option. The (faster) JS parser is used instead.').stack.replace('Error', 'Warning'))
  322. }
  323. }
  324. this.optionReturnBuffers = !!options.returnBuffers
  325. this.optionStringNumbers = !!options.stringNumbers
  326. this.returnError = options.returnError
  327. this.returnFatalError = options.returnFatalError || options.returnError
  328. this.returnReply = options.returnReply
  329. this.name = 'javascript'
  330. this.reset()
  331. }
  332. /**
  333. * Reset the parser values to the initial state
  334. *
  335. * @returns {undefined}
  336. */
  337. JavascriptRedisParser.prototype.reset = function () {
  338. this.offset = 0
  339. this.buffer = null
  340. this.bigStrSize = 0
  341. this.bigOffset = 0
  342. this.totalChunkSize = 0
  343. this.bufferCache = []
  344. this.arrayCache = []
  345. this.arrayPos = []
  346. }
  347. /**
  348. * Set the returnBuffers option
  349. *
  350. * @param returnBuffers
  351. * @returns {undefined}
  352. */
  353. JavascriptRedisParser.prototype.setReturnBuffers = function (returnBuffers) {
  354. if (typeof returnBuffers !== 'boolean') {
  355. throw new TypeError('The returnBuffers argument has to be a boolean')
  356. }
  357. this.optionReturnBuffers = returnBuffers
  358. }
  359. /**
  360. * Set the stringNumbers option
  361. *
  362. * @param stringNumbers
  363. * @returns {undefined}
  364. */
  365. JavascriptRedisParser.prototype.setStringNumbers = function (stringNumbers) {
  366. if (typeof stringNumbers !== 'boolean') {
  367. throw new TypeError('The stringNumbers argument has to be a boolean')
  368. }
  369. this.optionStringNumbers = stringNumbers
  370. }
  371. /**
  372. * Decrease the bufferPool size over time
  373. * @returns {undefined}
  374. */
  375. function decreaseBufferPool () {
  376. if (bufferPool.length > 50 * 1024) {
  377. // Balance between increasing and decreasing the bufferPool
  378. if (counter === 1 || notDecreased > counter * 2) {
  379. // Decrease the bufferPool by 10% by removing the first 10% of the current pool
  380. var sliceLength = Math.floor(bufferPool.length / 10)
  381. if (bufferOffset <= sliceLength) {
  382. bufferOffset = 0
  383. } else {
  384. bufferOffset -= sliceLength
  385. }
  386. bufferPool = bufferPool.slice(sliceLength, bufferPool.length)
  387. } else {
  388. notDecreased++
  389. counter--
  390. }
  391. } else {
  392. clearInterval(interval)
  393. counter = 0
  394. notDecreased = 0
  395. interval = null
  396. }
  397. }
  398. /**
  399. * Check if the requested size fits in the current bufferPool.
  400. * If it does not, reset and increase the bufferPool accordingly.
  401. *
  402. * @param length
  403. * @returns {undefined}
  404. */
  405. function resizeBuffer (length) {
  406. if (bufferPool.length < length + bufferOffset) {
  407. var multiplier = length > 1024 * 1024 * 75 ? 2 : 3
  408. if (bufferOffset > 1024 * 1024 * 111) {
  409. bufferOffset = 1024 * 1024 * 50
  410. }
  411. bufferPool = bufferAlloc(length * multiplier + bufferOffset)
  412. bufferOffset = 0
  413. counter++
  414. if (interval === null) {
  415. interval = setInterval(decreaseBufferPool, 50)
  416. }
  417. }
  418. }
  419. /**
  420. * Concat a bulk string containing multiple chunks
  421. *
  422. * Notes:
  423. * 1) The first chunk might contain the whole bulk string including the \r
  424. * 2) We are only safe to fully add up elements that are neither the first nor any of the last two elements
  425. *
  426. * @param parser
  427. * @returns {String}
  428. */
  429. function concatBulkString (parser) {
  430. var list = parser.bufferCache
  431. var chunks = list.length
  432. var offset = parser.bigStrSize - parser.totalChunkSize
  433. parser.offset = offset
  434. if (offset <= 2) {
  435. if (chunks === 2) {
  436. return list[0].toString('utf8', parser.bigOffset, list[0].length + offset - 2)
  437. }
  438. chunks--
  439. offset = list[list.length - 2].length + offset
  440. }
  441. var res = decoder.write(list[0].slice(parser.bigOffset))
  442. for (var i = 1; i < chunks - 1; i++) {
  443. res += decoder.write(list[i])
  444. }
  445. res += decoder.end(list[i].slice(0, offset - 2))
  446. return res
  447. }
  448. /**
  449. * Concat the collected chunks from parser.bufferCache.
  450. *
  451. * Increases the bufferPool size beforehand if necessary.
  452. *
  453. * @param parser
  454. * @returns {Buffer}
  455. */
  456. function concatBulkBuffer (parser) {
  457. var list = parser.bufferCache
  458. var chunks = list.length
  459. var length = parser.bigStrSize - parser.bigOffset - 2
  460. var offset = parser.bigStrSize - parser.totalChunkSize
  461. parser.offset = offset
  462. if (offset <= 2) {
  463. if (chunks === 2) {
  464. return list[0].slice(parser.bigOffset, list[0].length + offset - 2)
  465. }
  466. chunks--
  467. offset = list[list.length - 2].length + offset
  468. }
  469. resizeBuffer(length)
  470. var start = bufferOffset
  471. list[0].copy(bufferPool, start, parser.bigOffset, list[0].length)
  472. bufferOffset += list[0].length - parser.bigOffset
  473. for (var i = 1; i < chunks - 1; i++) {
  474. list[i].copy(bufferPool, bufferOffset)
  475. bufferOffset += list[i].length
  476. }
  477. list[i].copy(bufferPool, bufferOffset, 0, offset - 2)
  478. bufferOffset += offset - 2
  479. return bufferPool.slice(start, bufferOffset)
  480. }
  481. /**
  482. * Parse the redis buffer
  483. * @param buffer
  484. * @returns {undefined}
  485. */
  486. JavascriptRedisParser.prototype.execute = function execute (buffer) {
  487. if (this.buffer === null) {
  488. this.buffer = buffer
  489. this.offset = 0
  490. } else if (this.bigStrSize === 0) {
  491. var oldLength = this.buffer.length
  492. var remainingLength = oldLength - this.offset
  493. var newBuffer = bufferAlloc(remainingLength + buffer.length)
  494. this.buffer.copy(newBuffer, 0, this.offset, oldLength)
  495. buffer.copy(newBuffer, remainingLength, 0, buffer.length)
  496. this.buffer = newBuffer
  497. this.offset = 0
  498. if (this.arrayCache.length) {
  499. var arr = parseArrayChunks(this)
  500. if (!arr) {
  501. return
  502. }
  503. this.returnReply(arr)
  504. }
  505. } else if (this.totalChunkSize + buffer.length >= this.bigStrSize) {
  506. this.bufferCache.push(buffer)
  507. var tmp = this.optionReturnBuffers ? concatBulkBuffer(this) : concatBulkString(this)
  508. this.bigStrSize = 0
  509. this.bufferCache = []
  510. this.buffer = buffer
  511. if (this.arrayCache.length) {
  512. this.arrayCache[0][this.arrayPos[0]++] = tmp
  513. tmp = parseArrayChunks(this)
  514. if (!tmp) {
  515. return
  516. }
  517. }
  518. this.returnReply(tmp)
  519. } else {
  520. this.bufferCache.push(buffer)
  521. this.totalChunkSize += buffer.length
  522. return
  523. }
  524. while (this.offset < this.buffer.length) {
  525. var offset = this.offset
  526. var type = this.buffer[this.offset++]
  527. var response = parseType(this, type)
  528. if (response === undefined) {
  529. if (!this.arrayCache.length) {
  530. this.offset = offset
  531. }
  532. return
  533. }
  534. if (type === 45) {
  535. this.returnError(response)
  536. } else {
  537. this.returnReply(response)
  538. }
  539. }
  540. this.buffer = null
  541. }
  542. module.exports = JavascriptRedisParser