memcache-parser
Version:
A very efficient NodeJS memcached ASCII Protocol parser by using only Buffer APIs
203 lines • 8.16 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.MemcacheParser = void 0;
const tslib_1 = require("tslib");
const assert_1 = tslib_1.__importDefault(require("assert"));
/* eslint-disable no-magic-numbers, max-statements,no-var */
/* eslint max-len:[2,120] */
const log = (msg) => console.log(msg); // eslint-disable-line
const defaultLogger = {
error: log,
debug: log,
info: log,
warn: log,
};
const crlfBuf = Buffer.from("\r\n", "ascii");
class MemcacheParser {
constructor(logger) {
this._pending = undefined;
this._partialData = undefined;
this.logger = logger || defaultLogger;
this._cmdBrkLookupOffset = 0;
}
//
// call this with data received from the socket
//
onData(data) {
while (data !== undefined) {
data = this._processData(data);
}
}
//
// a client or server should override this
//
processCmd(cmdTokens) {
return cmdTokens.length;
}
//
// a client or server should override this
// see initiatePending below for info on result
//
receiveResult(result) {
return result;
}
//
// call this if a command expects data to follow
//
initiatePending(cmdTokens, length) {
(0, assert_1.default)(this._pending === undefined, "MemcacheParser: already waiting for data");
this._pending = {
data: Buffer.allocUnsafe(length),
filled: 0,
cmd: cmdTokens[0],
cmdTokens,
};
}
malformDataStream(pending, data, consumer // eslint-disable-line
) {
this.logger.error(`MemcacheParser: malformed memcache data stream, cmd: ${pending.cmd}` +
` remaining data length: ${data.length}`);
}
malformCommand(cmdTokens) {
this.logger.error(`MemcacheParser: malformed command: ${cmdTokens}`);
}
unknownCmd(cmdTokens) {
this.logger.error(`MemcacheParser: unknown command: ${cmdTokens}`);
}
//
// internal methods
//
_parseCmd(data) {
var _a, _b, _c;
(0, assert_1.default)(this._pending === undefined, "MemcacheParser: _parseCmd called with pending data");
const brkIdx = (_a = data === null || data === void 0 ? void 0 : data.indexOf(crlfBuf, this._cmdBrkLookupOffset)) !== null && _a !== void 0 ? _a : -1;
//
// Was a \r\n marker found
//
if (brkIdx >= 0) {
this._cmdBrkLookupOffset = 0;
//
// Slice just the command part up to the \r\n marker and split it into
// tokens separated by space
//
const cmdTokens = (_b = data === null || data === void 0 ? void 0 : data.slice(0, brkIdx).toString().split(" ")) !== null && _b !== void 0 ? _b : [];
//
// advance buffer to skip comand line + \r\n
// 1. If data contains enough to exactly hold the command and the \r\n marker,
// then just set it to undefined.
// 2. Otherwise slice it to right after the \r\n marker.
//
data = (data === null || data === void 0 ? void 0 : data.length) === brkIdx + 2 ? undefined : data === null || data === void 0 ? void 0 : data.slice(brkIdx + 2);
this.logger.debug(`MemcacheParser: got cmd, tokens: ${cmdTokens}`);
if (cmdTokens.length < 1 || cmdTokens[0].length < 1) {
this.malformCommand(cmdTokens);
}
else if (this.processCmd(cmdTokens) === 0) {
// This was false and not 0
this.unknownCmd(cmdTokens);
data = undefined;
}
else if (data !== undefined && this._pending !== undefined) {
data = this._copyPending(data);
}
}
else {
// \r\n marker not found.
this.logger.debug(`MemcacheParser: _parseCmd no linebreak, storing data for later, length: ${data === null || data === void 0 ? void 0 : data.length}`);
// 1. We need more data until we see \r\n so save what we have now as partial
this._partialData = data;
// 2. In case the data ends at just \r, when we get more data, we should
// start looking for \r\n one byte back.
this._cmdBrkLookupOffset = ((_c = data === null || data === void 0 ? void 0 : data.length) !== null && _c !== void 0 ? _c : 0) - 1;
data = undefined;
}
return data;
}
//
// When a command expects data to follow, this copies the chunks
// into a single buffer until the expected bytes are received.
//
_copyPending(data) {
if (data === undefined) {
return undefined;
}
var consumed = 0;
const pending = this._pending;
//
// pending still needs more data to fill up.
//
if (pending.filled < pending.data.length) {
//
// - Copy from data starting at index 0, into pending data, start at where
// it's currently filled up to.
// - If data has more than enough to fill up pending, then the copy will
// stop when pending is filled up and return the size consumed from data.
//
consumed = data.copy(pending.data, pending.filled, 0);
pending.filled += consumed;
}
//
// Was there enough data to fill up pending?
//
if (pending.filled === pending.data.length) {
const remaining = data.length - consumed;
// Are there still enough leftover in data to hold the \r\n end mark?
if (remaining >= 2) {
// Look for \r\n after the data
if (data[consumed] !== 13 || data[consumed + 1] !== 10) {
//
// We got all the data but it's not followed by CR and LF?
//
this.malformDataStream(pending, data, consumed);
consumed = data.length;
}
else {
consumed += 2; // skip \r\n
this._pending = undefined;
this.receiveResult(pending);
}
return data.length > consumed ? data.slice(consumed) : undefined;
}
else if (remaining > 0) {
// Woo, exactly 1 byte left!
// Need to save what's left into partial data for next round
this._partialData = data.slice(consumed);
}
}
return undefined;
}
_checkPartialData(data) {
// was there data that didn't have the \r\n marker?
if (this._partialData !== undefined) {
const partial = this._partialData;
this._partialData = undefined;
const newLength = partial.length + data.length;
//
// If no pending data and partial data is longer than 512, then reset because
// we don't expect command line to be longer than that.
//
if (this._pending === undefined && partial.length > 512 && newLength > 2000) {
this.logger.warn(`MemcacheParser: partial data length ${partial.length} ` +
`and new data length ${newLength} too big, resetting`);
return undefined;
}
this.logger.debug(`MemcacheParser: concat partial data, length: ` +
`${partial.length}, new length ${newLength} pending ${this._pending}`);
// Join previous partial and new data into a single bigger data
data = Buffer.concat([partial, data], newLength);
}
return data;
}
_processData(data) {
data = this._checkPartialData(data);
if (this._pending !== undefined) {
data = this._copyPending(data);
}
if (data !== undefined) {
data = this._parseCmd(data);
}
return data;
}
}
exports.MemcacheParser = MemcacheParser;
//# sourceMappingURL=memcache-parser.js.map