You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

543 lines
16 KiB
JavaScript

/*
* Copyright DataStax, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
const util = require('util');
const utils = require('./utils');
const types = require('./types');
const errors = require('./errors');
/**
* Information on the formatting of the returned rows
*/
const resultFlag = {
globalTablesSpec: 0x0001,
hasMorePages: 0x0002,
noMetadata: 0x0004,
metadataChanged: 0x0008,
continuousPaging: 0x40000000,
lastContinuousPage: 0x80000000,
};
// templates for derived error messages.
const _writeTimeoutQueryMessage = 'Server timeout during write query at consistency %s (%d peer(s) acknowledged the write over %d required)';
const _writeTimeoutBatchLogMessage = 'Server timeout during batchlog write at consistency %s (%d peer(s) acknowledged the write over %d required)';
const _writeFailureMessage = 'Server failure during write query at consistency %s (%d responses were required but only %d replicas responded, %d failed)';
const _unavailableMessage = 'Not enough replicas available for query at consistency %s (%d required but only %d alive)';
const _readTimeoutMessage = 'Server timeout during read query at consistency %s (%s)';
const _readFailureMessage = 'Server failure during read query at consistency %s (%d responses were required but only %d replicas responded, %d failed)';
/**
* Buffer forward reader of CQL binary frames
* @param {FrameHeader} header
* @param {Buffer} body
* @param {Number} [offset]
*/
class FrameReader {
/**
* Creates a new instance of the reader
* @param {FrameHeader} header
* @param {Buffer} body
* @param {Number} [offset]
*/
constructor(header, body, offset) {
this.header = header;
this.opcode = header.opcode;
this.offset = offset || 0;
this.buf = body;
}
remainingLength() {
return this.buf.length - this.offset;
}
getBuffer() {
return this.buf;
}
/**
* Slices the underlining buffer
* @param {Number} begin
* @param {Number} [end]
* @returns {Buffer}
*/
slice(begin, end) {
if (typeof end === 'undefined') {
end = this.buf.length;
}
return this.buf.slice(begin, end);
}
/**
* Modifies the underlying buffer, it concatenates the given buffer with the original (internalBuffer = concat(bytes, internalBuffer)
*/
unshift(bytes) {
if (this.offset > 0) {
throw new Error('Can not modify the underlying buffer if already read');
}
this.buf = Buffer.concat([bytes, this.buf], bytes.length + this.buf.length);
}
/**
* Reads any number of bytes and moves the offset.
* if length not provided or it's larger than the remaining bytes, reads to end.
* @param length
* @returns {Buffer}
*/
read(length) {
let end = this.buf.length;
if (typeof length !== 'undefined' && this.offset + length < this.buf.length) {
end = this.offset + length;
}
const bytes = this.slice(this.offset, end);
this.offset = end;
return bytes;
}
/**
* Moves the reader cursor to the end
*/
toEnd() {
this.offset = this.buf.length;
}
/**
* Reads a BE Int and moves the offset
* @returns {Number}
*/
readInt() {
const result = this.buf.readInt32BE(this.offset);
this.offset += 4;
return result;
}
/** @returns {Number} */
readShort() {
const result = this.buf.readUInt16BE(this.offset);
this.offset += 2;
return result;
}
readByte() {
const result = this.buf.readUInt8(this.offset);
this.offset += 1;
return result;
}
readString() {
const length = this.readShort();
this.checkOffset(length);
const result = this.buf.toString('utf8', this.offset, this.offset + length);
this.offset += length;
return result;
}
/**
* Checks that the new length to read is within the range of the buffer length. Throws a RangeError if not.
* @param {Number} newLength
*/
checkOffset(newLength) {
if (this.offset + newLength > this.buf.length) {
const err = new RangeError('Trying to access beyond buffer length');
err.expectedLength = newLength;
throw err;
}
}
/**
* Reads a protocol string list
* @returns {Array}
*/
readStringList() {
const length = this.readShort();
const list = new Array(length);
for (let i = 0; i < length; i++) {
list[i] = this.readString();
}
return list;
}
/**
* Reads the amount of bytes that the field has and returns them (slicing them).
* @returns {Buffer}
*/
readBytes() {
const length = this.readInt();
if (length < 0) {
return null;
}
this.checkOffset(length);
return this.read(length);
}
readShortBytes() {
const length = this.readShort();
if (length < 0) {
return null;
}
this.checkOffset(length);
return this.read(length);
}
/**
* Reads an associative array of strings as keys and bytes as values
* @param {Number} length
* @param {Function} keyFn
* @param {Function} valueFn
* @returns {Object}
*/
readMap(length, keyFn, valueFn) {
if (length < 0) {
return null;
}
const map = {};
for (let i = 0; i < length; i++) {
map[keyFn.call(this)] = valueFn.call(this);
}
return map;
}
/**
* Reads an associative array of strings as keys and string lists as values
* @returns {Object}
*/
readStringMultiMap() {
//A [short] n, followed by n pair <k><v> where <k> is a
//[string] and <v> is a [string[]].
const length = this.readShort();
if (length < 0) {
return null;
}
const map = {};
for (let i = 0; i < length; i++) {
map[this.readString()] = this.readStringList();
}
return map;
}
/**
* Reads a data type definition
* @returns {{code: Number, info: Object|null}} An array of 2 elements
*/
readType() {
let i;
const type = {
code: this.readShort(),
type: null
};
switch (type.code) {
case types.dataTypes.custom:
type.info = this.readString();
break;
case types.dataTypes.list:
case types.dataTypes.set:
type.info = this.readType();
break;
case types.dataTypes.map:
type.info = [this.readType(), this.readType()];
break;
case types.dataTypes.udt:
type.info = {
keyspace: this.readString(),
name: this.readString(),
fields: new Array(this.readShort())
};
for (i = 0; i < type.info.fields.length; i++) {
type.info.fields[i] = {
name: this.readString(),
type: this.readType()
};
}
break;
case types.dataTypes.tuple:
type.info = new Array(this.readShort());
for (i = 0; i < type.info.length; i++) {
type.info[i] = this.readType();
}
break;
}
return type;
}
/**
* Reads an Ip address and port
* @returns {{address: exports.InetAddress, port: Number}}
*/
readInet() {
const length = this.readByte();
const address = this.read(length);
return { address: new types.InetAddress(address), port: this.readInt() };
}
/**
* Reads an Ip address
* @returns {InetAddress}
*/
readInetAddress() {
const length = this.readByte();
return new types.InetAddress(this.read(length));
}
/**
* Reads the body bytes corresponding to the flags
* @returns {{traceId: Uuid, warnings: Array, customPayload}}
* @throws {RangeError}
*/
readFlagsInfo() {
if (this.header.flags === 0) {
return utils.emptyObject;
}
const result = {};
if (this.header.flags & types.frameFlags.tracing) {
this.checkOffset(16);
result.traceId = new types.Uuid(utils.copyBuffer(this.read(16)));
}
if (this.header.flags & types.frameFlags.warning) {
result.warnings = this.readStringList();
}
if (this.header.flags & types.frameFlags.customPayload) {
// Custom payload is a Map<string, Buffer>
result.customPayload = this.readMap(this.readShort(), this.readString, this.readBytes);
}
return result;
}
/**
* Reads the metadata from a row or a prepared result response
* @param {Number} kind
* @returns {Object}
* @throws {RangeError}
*/
readMetadata(kind) {
let i;
//Determines if its a prepared metadata
const isPrepared = (kind === types.resultKind.prepared);
const meta = {};
if (types.protocolVersion.supportsResultMetadataId(this.header.version) && isPrepared) {
meta.resultId = utils.copyBuffer(this.readShortBytes());
}
//as used in Rows and Prepared responses
const flags = this.readInt();
const columnLength = this.readInt();
if (types.protocolVersion.supportsPreparedPartitionKey(this.header.version) && isPrepared) {
//read the pk columns
meta.partitionKeys = new Array(this.readInt());
for (i = 0; i < meta.partitionKeys.length; i++) {
meta.partitionKeys[i] = this.readShort();
}
}
if (flags & resultFlag.hasMorePages) {
meta.pageState = utils.copyBuffer(this.readBytes());
}
if (flags & resultFlag.metadataChanged) {
meta.newResultId = utils.copyBuffer(this.readShortBytes());
}
if (flags & resultFlag.continuousPaging) {
meta.continuousPageIndex = this.readInt();
meta.lastContinuousPage = !!(flags & resultFlag.lastContinuousPage);
}
if (flags & resultFlag.globalTablesSpec) {
meta.global_tables_spec = true;
meta.keyspace = this.readString();
meta.table = this.readString();
}
meta.columns = new Array(columnLength);
meta.columnsByName = utils.emptyObject;
if (isPrepared) {
//for prepared metadata, we will need a index of the columns (param) by name
meta.columnsByName = {};
}
for (i = 0; i < columnLength; i++) {
const col = {};
if (!meta.global_tables_spec) {
col.ksname = this.readString();
col.tablename = this.readString();
}
col.name = this.readString();
col.type = this.readType();
meta.columns[i] = col;
if (isPrepared) {
meta.columnsByName[col.name] = i;
}
}
return meta;
}
/**
* Reads the error from the frame
* @throws {RangeError}
* @returns {ResponseError}
*/
readError() {
const code = this.readInt();
const message = this.readString();
const err = new errors.ResponseError(code, message);
//read extra info
switch (code) {
case types.responseErrorCodes.unavailableException:
err.consistencies = this.readShort();
err.required = this.readInt();
err.alive = this.readInt();
err.message = util.format(_unavailableMessage, types.consistencyToString[err.consistencies], err.required, err.alive);
break;
case types.responseErrorCodes.readTimeout:
case types.responseErrorCodes.readFailure:
err.consistencies = this.readShort();
err.received = this.readInt();
err.blockFor = this.readInt();
if (code === types.responseErrorCodes.readFailure) {
if (types.protocolVersion.supportsFailureReasonMap(this.header.version)) {
err.failures = this.readInt();
err.reasons = this.readMap(err.failures, this.readInetAddress, this.readShort);
}
else {
err.failures = this.readInt();
}
}
err.isDataPresent = this.readByte();
if (code === types.responseErrorCodes.readTimeout) {
let details;
if (err.received < err.blockFor) {
details = util.format('%d replica(s) responded over %d required', err.received, err.blockFor);
}
else if (!err.isDataPresent) {
details = 'the replica queried for the data didn\'t respond';
}
else {
details = 'timeout while waiting for repair of inconsistent replica';
}
err.message = util.format(_readTimeoutMessage, types.consistencyToString[err.consistencies], details);
}
else {
err.message = util.format(_readFailureMessage, types.consistencyToString[err.consistencies], err.blockFor, err.received, err.failures);
}
break;
case types.responseErrorCodes.writeTimeout:
case types.responseErrorCodes.writeFailure:
err.consistencies = this.readShort();
err.received = this.readInt();
err.blockFor = this.readInt();
if (code === types.responseErrorCodes.writeFailure) {
if (types.protocolVersion.supportsFailureReasonMap(this.header.version)) {
err.failures = this.readInt();
err.reasons = this.readMap(err.failures, this.readInetAddress, this.readShort);
}
else {
err.failures = this.readInt();
}
}
err.writeType = this.readString();
if (code === types.responseErrorCodes.writeTimeout) {
const template = err.writeType === 'BATCH_LOG' ? _writeTimeoutBatchLogMessage : _writeTimeoutQueryMessage;
err.message = util.format(template, types.consistencyToString[err.consistencies], err.received, err.blockFor);
}
else {
err.message = util.format(_writeFailureMessage, types.consistencyToString[err.consistencies], err.blockFor, err.received, err.failures);
}
break;
case types.responseErrorCodes.unprepared:
err.queryId = utils.copyBuffer(this.readShortBytes());
break;
case types.responseErrorCodes.functionFailure:
err.keyspace = this.readString();
err.functionName = this.readString();
err.argTypes = this.readStringList();
break;
case types.responseErrorCodes.alreadyExists: {
err.keyspace = this.readString();
const table = this.readString();
if (table.length > 0) {
err.table = table;
}
break;
}
}
return err;
}
/**
* Reads an event from Cassandra and returns the detail
* @returns {{eventType: String, inet: {address: Buffer, port: Number}}, *}
*/
readEvent() {
const eventType = this.readString();
switch (eventType) {
case types.protocolEvents.topologyChange:
return {
added: this.readString() === 'NEW_NODE',
inet: this.readInet(),
eventType: eventType
};
case types.protocolEvents.statusChange:
return {
up: this.readString() === 'UP',
inet: this.readInet(),
eventType: eventType
};
case types.protocolEvents.schemaChange:
return this.parseSchemaChange();
}
//Forward compatibility
return { eventType: eventType };
}
parseSchemaChange() {
let result;
if (!types.protocolVersion.supportsSchemaChangeFullMetadata(this.header.version)) {
//v1/v2: 3 strings, the table value can be empty
result = {
eventType: types.protocolEvents.schemaChange,
schemaChangeType: this.readString(),
keyspace: this.readString(),
table: this.readString()
};
result.isKeyspace = !result.table;
return result;
}
//v3+: 3 or 4 strings: change_type, target, keyspace and (table, type, functionName or aggregate)
result = {
eventType: types.protocolEvents.schemaChange,
schemaChangeType: this.readString(),
target: this.readString(),
keyspace: this.readString(),
table: null,
udt: null,
signature: null
};
result.isKeyspace = result.target === 'KEYSPACE';
switch (result.target) {
case 'TABLE':
result.table = this.readString();
break;
case 'TYPE':
result.udt = this.readString();
break;
case 'FUNCTION':
result.functionName = this.readString();
result.signature = this.readStringList();
break;
case 'AGGREGATE':
result.aggregate = this.readString();
result.signature = this.readStringList();
}
return result;
}
}
module.exports = { FrameReader };