diff --git a/src/metadata-parser.ts b/src/metadata-parser.ts index ea1ed1b43..8dc33e801 100644 --- a/src/metadata-parser.ts +++ b/src/metadata-parser.ts @@ -5,6 +5,8 @@ import { type CryptoMetadata } from './always-encrypted/types'; import { sprintf } from 'sprintf-js'; +import { Result, NotEnoughDataError, readUInt8, readBVarChar, readUsVarChar, readUInt16LE, readUInt32LE } from './token/helpers'; + interface XmlSchema { dbname: string; owningSchema: string; @@ -53,272 +55,329 @@ export type Metadata = { cryptoMetadata?: CryptoMetadata; } & BaseMetadata; +function readCollation(buf: Buffer, offset: number): Result { + offset = +offset; + + if (buf.length < offset + 5) { + throw new NotEnoughDataError(offset + 5); + } -function readCollation(parser: Parser, callback: (collation: Collation) => void) { - // s2.2.5.1.2 - parser.readBuffer(5, (collationData) => { - callback(Collation.fromBuffer(collationData)); - }); + const collation = Collation.fromBuffer(buf.slice(offset, offset + 5)); + return new Result(collation, offset + 5); } -function readSchema(parser: Parser, callback: (schema: XmlSchema | undefined) => void) { - // s2.2.5.5.3 - parser.readUInt8((schemaPresent) => { - if (schemaPresent === 0x01) { - parser.readBVarChar((dbname) => { - parser.readBVarChar((owningSchema) => { - parser.readUsVarChar((xmlSchemaCollection) => { - callback({ - dbname: dbname, - owningSchema: owningSchema, - xmlSchemaCollection: xmlSchemaCollection - }); - }); - }); - }); - } else { - callback(undefined); - } - }); +function readSchema(buf: Buffer, offset: number): Result { + offset = +offset; + + let schemaPresent; + ({ offset, value: schemaPresent } = readUInt8(buf, offset)); + + if (schemaPresent !== 0x01) { + return new Result(undefined, offset); + } + + let dbname; + ({ offset, value: dbname } = readBVarChar(buf, offset)); + + let owningSchema; + ({ offset, value: owningSchema } = readBVarChar(buf, offset)); + + let xmlSchemaCollection; + ({ offset, value: xmlSchemaCollection } = readUsVarChar(buf, offset)); + + return new Result({ dbname, owningSchema, xmlSchemaCollection }, offset); } -function readUDTInfo(parser: Parser, callback: (udtInfo: UdtInfo | undefined) => void) { - parser.readUInt16LE((maxByteSize) => { - parser.readBVarChar((dbname) => { - parser.readBVarChar((owningSchema) => { - parser.readBVarChar((typeName) => { - parser.readUsVarChar((assemblyName) => { - callback({ - maxByteSize: maxByteSize, - dbname: dbname, - owningSchema: owningSchema, - typeName: typeName, - assemblyName: assemblyName - }); - }); - }); - }); - }); - }); +function readUDTInfo(buf: Buffer, offset: number): Result { + let maxByteSize; + ({ offset, value: maxByteSize } = readUInt16LE(buf, offset)); + + let dbname; + ({ offset, value: dbname } = readBVarChar(buf, offset)); + + let owningSchema; + ({ offset, value: owningSchema } = readBVarChar(buf, offset)); + + let typeName; + ({ offset, value: typeName } = readBVarChar(buf, offset)); + + let assemblyName; + ({ offset, value: assemblyName } = readUsVarChar(buf, offset)); + + return new Result({ + maxByteSize: maxByteSize, + dbname: dbname, + owningSchema: owningSchema, + typeName: typeName, + assemblyName: assemblyName + }, offset); } -function metadataParse(parser: Parser, options: ParserOptions, callback: (metadata: Metadata) => void) { - (options.tdsVersion < '7_2' ? parser.readUInt16LE : parser.readUInt32LE).call(parser, (userType) => { - parser.readUInt16LE((flags) => { - parser.readUInt8((typeNumber) => { - const type: DataType = TYPE[typeNumber]; +function readMetadata(buf: Buffer, offset: number, options: ParserOptions): Result { + let userType; + ({ offset, value: userType } = (options.tdsVersion < '7_2' ? readUInt16LE : readUInt32LE)(buf, offset)); - if (!type) { - throw new Error(sprintf('Unrecognised data type 0x%02X', typeNumber)); - } + let flags; + ({ offset, value: flags } = readUInt16LE(buf, offset)); + + let typeNumber; + ({ offset, value: typeNumber } = readUInt8(buf, offset)); + + const type: DataType = TYPE[typeNumber]; + if (!type) { + throw new Error(sprintf('Unrecognised data type 0x%02X', typeNumber)); + } + + switch (type.name) { + case 'Null': + case 'TinyInt': + case 'SmallInt': + case 'Int': + case 'BigInt': + case 'Real': + case 'Float': + case 'SmallMoney': + case 'Money': + case 'Bit': + case 'SmallDateTime': + case 'DateTime': + case 'Date': + return new Result({ + userType: userType, + flags: flags, + type: type, + collation: undefined, + precision: undefined, + scale: undefined, + dataLength: undefined, + schema: undefined, + udtInfo: undefined + }, offset); + + case 'IntN': + case 'FloatN': + case 'MoneyN': + case 'BitN': + case 'UniqueIdentifier': + case 'DateTimeN': { + let dataLength; + ({ offset, value: dataLength } = readUInt8(buf, offset)); + + return new Result({ + userType: userType, + flags: flags, + type: type, + collation: undefined, + precision: undefined, + scale: undefined, + dataLength: dataLength, + schema: undefined, + udtInfo: undefined + }, offset); + } + + case 'Variant': { + let dataLength; + ({ offset, value: dataLength } = readUInt32LE(buf, offset)); + + return new Result({ + userType: userType, + flags: flags, + type: type, + collation: undefined, + precision: undefined, + scale: undefined, + dataLength: dataLength, + schema: undefined, + udtInfo: undefined + }, offset); + } + + case 'VarChar': + case 'Char': + case 'NVarChar': + case 'NChar': { + let dataLength; + ({ offset, value: dataLength } = readUInt16LE(buf, offset)); + + let collation; + ({ offset, value: collation } = readCollation(buf, offset)); + + return new Result({ + userType: userType, + flags: flags, + type: type, + collation: collation, + precision: undefined, + scale: undefined, + dataLength: dataLength, + schema: undefined, + udtInfo: undefined + }, offset); + } + + case 'Text': + case 'NText': { + let dataLength; + ({ offset, value: dataLength } = readUInt32LE(buf, offset)); - switch (type.name) { - case 'Null': - case 'TinyInt': - case 'SmallInt': - case 'Int': - case 'BigInt': - case 'Real': - case 'Float': - case 'SmallMoney': - case 'Money': - case 'Bit': - case 'SmallDateTime': - case 'DateTime': - case 'Date': - return callback({ - userType: userType, - flags: flags, - type: type, - collation: undefined, - precision: undefined, - scale: undefined, - dataLength: undefined, - schema: undefined, - udtInfo: undefined - }); - - case 'IntN': - case 'FloatN': - case 'MoneyN': - case 'BitN': - case 'UniqueIdentifier': - case 'DateTimeN': - return parser.readUInt8((dataLength) => { - callback({ - userType: userType, - flags: flags, - type: type, - collation: undefined, - precision: undefined, - scale: undefined, - dataLength: dataLength, - schema: undefined, - udtInfo: undefined - }); - }); - - case 'Variant': - return parser.readUInt32LE((dataLength) => { - callback({ - userType: userType, - flags: flags, - type: type, - collation: undefined, - precision: undefined, - scale: undefined, - dataLength: dataLength, - schema: undefined, - udtInfo: undefined - }); - }); - - case 'VarChar': - case 'Char': - case 'NVarChar': - case 'NChar': - return parser.readUInt16LE((dataLength) => { - readCollation(parser, (collation) => { - callback({ - userType: userType, - flags: flags, - type: type, - collation: collation, - precision: undefined, - scale: undefined, - dataLength: dataLength, - schema: undefined, - udtInfo: undefined - }); - }); - }); - - case 'Text': - case 'NText': - return parser.readUInt32LE((dataLength) => { - readCollation(parser, (collation) => { - callback({ - userType: userType, - flags: flags, - type: type, - collation: collation, - precision: undefined, - scale: undefined, - dataLength: dataLength, - schema: undefined, - udtInfo: undefined - }); - }); - }); - - case 'VarBinary': - case 'Binary': - return parser.readUInt16LE((dataLength) => { - callback({ - userType: userType, - flags: flags, - type: type, - collation: undefined, - precision: undefined, - scale: undefined, - dataLength: dataLength, - schema: undefined, - udtInfo: undefined - }); - }); - - case 'Image': - return parser.readUInt32LE((dataLength) => { - callback({ - userType: userType, - flags: flags, - type: type, - collation: undefined, - precision: undefined, - scale: undefined, - dataLength: dataLength, - schema: undefined, - udtInfo: undefined - }); - }); - - case 'Xml': - return readSchema(parser, (schema) => { - callback({ - userType: userType, - flags: flags, - type: type, - collation: undefined, - precision: undefined, - scale: undefined, - dataLength: undefined, - schema: schema, - udtInfo: undefined - }); - }); - - case 'Time': - case 'DateTime2': - case 'DateTimeOffset': - return parser.readUInt8((scale) => { - callback({ - userType: userType, - flags: flags, - type: type, - collation: undefined, - precision: undefined, - scale: scale, - dataLength: undefined, - schema: undefined, - udtInfo: undefined - }); - }); - - case 'NumericN': - case 'DecimalN': - return parser.readUInt8((dataLength) => { - parser.readUInt8((precision) => { - parser.readUInt8((scale) => { - callback({ - userType: userType, - flags: flags, - type: type, - collation: undefined, - precision: precision, - scale: scale, - dataLength: dataLength, - schema: undefined, - udtInfo: undefined - }); - }); - }); - }); - - case 'UDT': - return readUDTInfo(parser, (udtInfo) => { - callback({ - userType: userType, - flags: flags, - type: type, - collation: undefined, - precision: undefined, - scale: undefined, - dataLength: undefined, - schema: undefined, - udtInfo: udtInfo - }); - }); - - default: - throw new Error(sprintf('Unrecognised type %s', type.name)); + let collation; + ({ offset, value: collation } = readCollation(buf, offset)); + + return new Result({ + userType: userType, + flags: flags, + type: type, + collation: collation, + precision: undefined, + scale: undefined, + dataLength: dataLength, + schema: undefined, + udtInfo: undefined + }, offset); + } + + case 'VarBinary': + case 'Binary': { + let dataLength; + ({ offset, value: dataLength } = readUInt16LE(buf, offset)); + + return new Result({ + userType: userType, + flags: flags, + type: type, + collation: undefined, + precision: undefined, + scale: undefined, + dataLength: dataLength, + schema: undefined, + udtInfo: undefined + }, offset); + } + + case 'Image': { + let dataLength; + ({ offset, value: dataLength } = readUInt32LE(buf, offset)); + + return new Result({ + userType: userType, + flags: flags, + type: type, + collation: undefined, + precision: undefined, + scale: undefined, + dataLength: dataLength, + schema: undefined, + udtInfo: undefined + }, offset); + } + + case 'Xml': { + let schema; + ({ offset, value: schema } = readSchema(buf, offset)); + + return new Result({ + userType: userType, + flags: flags, + type: type, + collation: undefined, + precision: undefined, + scale: undefined, + dataLength: undefined, + schema: schema, + udtInfo: undefined + }, offset); + } + + case 'Time': + case 'DateTime2': + case 'DateTimeOffset': { + let scale; + ({ offset, value: scale } = readUInt8(buf, offset)); + + return new Result({ + userType: userType, + flags: flags, + type: type, + collation: undefined, + precision: undefined, + scale: scale, + dataLength: undefined, + schema: undefined, + udtInfo: undefined + }, offset); + } + + case 'NumericN': + case 'DecimalN': { + let dataLength; + ({ offset, value: dataLength } = readUInt8(buf, offset)); + + let precision; + ({ offset, value: precision } = readUInt8(buf, offset)); + + let scale; + ({ offset, value: scale } = readUInt8(buf, offset)); + + return new Result({ + userType: userType, + flags: flags, + type: type, + collation: undefined, + precision: precision, + scale: scale, + dataLength: dataLength, + schema: undefined, + udtInfo: undefined + }, offset); + } + + case 'UDT': { + let udtInfo; + ({ offset, value: udtInfo } = readUDTInfo(buf, offset)); + + return new Result({ + userType: userType, + flags: flags, + type: type, + collation: undefined, + precision: undefined, + scale: undefined, + dataLength: undefined, + schema: undefined, + udtInfo: udtInfo + }, offset); + } + + default: + throw new Error(sprintf('Unrecognised type %s', type.name)); + } +} + +function metadataParse(parser: Parser, options: ParserOptions, callback: (metadata: Metadata) => void) { + (async () => { + while (true) { + let result; + try { + result = readMetadata(parser.buffer, parser.position, options); + } catch (err: any) { + if (err instanceof NotEnoughDataError) { + await parser.waitForChunk(); + continue; } - }); - }); - }); + + throw err; + } + + parser.position = result.offset; + return callback(result.value); + } + })(); } export default metadataParse; -export { readCollation }; +export { readCollation, readMetadata }; module.exports = metadataParse; module.exports.readCollation = readCollation; +module.exports.readMetadata = readMetadata; diff --git a/src/token/colmetadata-token-parser.ts b/src/token/colmetadata-token-parser.ts index 954ec3329..1b6d111e1 100644 --- a/src/token/colmetadata-token-parser.ts +++ b/src/token/colmetadata-token-parser.ts @@ -1,7 +1,8 @@ -import metadataParse, { type Metadata } from '../metadata-parser'; +import { readMetadata, type Metadata } from '../metadata-parser'; import Parser, { type ParserOptions } from './stream-parser'; import { ColMetadataToken } from './token'; +import { NotEnoughDataError, Result, readBVarChar, readUInt16LE, readUInt8, readUsVarChar } from './helpers'; export interface ColumnMetadata extends Metadata { /** @@ -12,101 +13,112 @@ export interface ColumnMetadata extends Metadata { tableName?: string | string[] | undefined; } -function readTableName(parser: Parser, options: ParserOptions, metadata: Metadata, callback: (tableName?: string | string[]) => void) { - if (metadata.type.hasTableName) { - if (options.tdsVersion >= '7_2') { - parser.readUInt8((numberOfTableNameParts) => { - const tableName: string[] = []; +function readTableName(buf: Buffer, offset: number, metadata: Metadata, options: ParserOptions): Result { + if (!metadata.type.hasTableName) { + return new Result(undefined, offset); + } - let i = 0; - function next(done: () => void) { - if (numberOfTableNameParts === i) { - return done(); - } + if (options.tdsVersion < '7_2') { + return readUsVarChar(buf, offset); + } - parser.readUsVarChar((part) => { - tableName.push(part); + let numberOfTableNameParts; + ({ offset, value: numberOfTableNameParts } = readUInt8(buf, offset)); - i++; + const tableName: string[] = []; + for (let i = 0; i < numberOfTableNameParts; i++) { + let tableNamePart; + ({ offset, value: tableNamePart } = readUsVarChar(buf, offset)); - next(done); - }); - } - - next(() => { - callback(tableName); - }); - }); - } else { - parser.readUsVarChar(callback); - } - } else { - callback(undefined); + tableName.push(tableNamePart); } + + return new Result(tableName, offset); } -function readColumnName(parser: Parser, options: ParserOptions, index: number, metadata: Metadata, callback: (colName: string) => void) { - parser.readBVarChar((colName) => { - if (options.columnNameReplacer) { - callback(options.columnNameReplacer(colName, index, metadata)); - } else if (options.camelCaseColumns) { - callback(colName.replace(/^[A-Z]/, function(s) { - return s.toLowerCase(); - })); - } else { - callback(colName); - } - }); +function readColumnName(buf: Buffer, offset: number, index: number, metadata: Metadata, options: ParserOptions): Result { + let colName; + ({ offset, value: colName } = readBVarChar(buf, offset)); + + if (options.columnNameReplacer) { + return new Result(options.columnNameReplacer(colName, index, metadata), offset); + } else if (options.camelCaseColumns) { + return new Result(colName.replace(/^[A-Z]/, function(s) { + return s.toLowerCase(); + }), offset); + } else { + return new Result(colName, offset); + } } -function readColumn(parser: Parser, options: ParserOptions, index: number, callback: (column: ColumnMetadata) => void) { - metadataParse(parser, options, (metadata) => { - readTableName(parser, options, metadata, (tableName) => { - readColumnName(parser, options, index, metadata, (colName) => { - callback({ - userType: metadata.userType, - flags: metadata.flags, - type: metadata.type, - collation: metadata.collation, - precision: metadata.precision, - scale: metadata.scale, - udtInfo: metadata.udtInfo, - dataLength: metadata.dataLength, - schema: metadata.schema, - colName: colName, - tableName: tableName - }); - }); - }); - }); +function readColumn(buf: Buffer, offset: number, options: ParserOptions, index: number) { + let metadata; + ({ offset, value: metadata } = readMetadata(buf, offset, options)); + + let tableName; + ({ offset, value: tableName } = readTableName(buf, offset, metadata, options)); + + let colName; + ({ offset, value: colName } = readColumnName(buf, offset, index, metadata, options)); + + return new Result({ + userType: metadata.userType, + flags: metadata.flags, + type: metadata.type, + collation: metadata.collation, + precision: metadata.precision, + scale: metadata.scale, + udtInfo: metadata.udtInfo, + dataLength: metadata.dataLength, + schema: metadata.schema, + colName: colName, + tableName: tableName + }, offset); } async function colMetadataParser(parser: Parser): Promise { - while (parser.buffer.length - parser.position < 2) { - await parser.streamBuffer.waitForChunk(); - } + let columnCount; + + while (true) { + let offset; + + try { + ({ offset, value: columnCount } = readUInt16LE(parser.buffer, parser.position)); + } catch (err) { + if (err instanceof NotEnoughDataError) { + await parser.waitForChunk(); + continue; + } + + throw err; + } - const columnCount = parser.buffer.readUInt16LE(parser.position); - parser.position += 2; + parser.position = offset; + break; + } const columns: ColumnMetadata[] = []; for (let i = 0; i < columnCount; i++) { - let column: ColumnMetadata; - - readColumn(parser, parser.options, i, (c) => { - column = c; - }); + while (true) { + let column: ColumnMetadata; + let offset; + + try { + ({ offset, value: column } = readColumn(parser.buffer, parser.position, parser.options, i)); + } catch (err: any) { + if (err instanceof NotEnoughDataError) { + await parser.waitForChunk(); + continue; + } - while (parser.suspended) { - await parser.streamBuffer.waitForChunk(); + throw err; + } - parser.suspended = false; - const next = parser.next!; + parser.position = offset; + columns.push(column); - next(); + break; } - - columns.push(column!); } return new ColMetadataToken(columns); diff --git a/src/token/done-token-parser.ts b/src/token/done-token-parser.ts index 0db7642dd..9ef40224c 100644 --- a/src/token/done-token-parser.ts +++ b/src/token/done-token-parser.ts @@ -1,5 +1,6 @@ -import Parser, { type ParserOptions } from './stream-parser'; +import { type ParserOptions } from './stream-parser'; import { DoneToken, DoneInProcToken, DoneProcToken } from './token'; +import { Result, readBigUInt64LE, readUInt16LE, readUInt32LE } from './helpers'; // s2.2.7.5/6/7 @@ -22,51 +23,46 @@ interface TokenData { curCmd: number; } -function parseToken(parser: Parser, options: ParserOptions, callback: (data: TokenData) => void) { - parser.readUInt16LE((status) => { - const more = !!(status & STATUS.MORE); - const sqlError = !!(status & STATUS.ERROR); - const rowCountValid = !!(status & STATUS.COUNT); - const attention = !!(status & STATUS.ATTN); - const serverError = !!(status & STATUS.SRVERROR); +function readToken(buf: Buffer, offset: number, options: ParserOptions): Result { + let status; + ({ offset, value: status } = readUInt16LE(buf, offset)); - parser.readUInt16LE((curCmd) => { - const next = (rowCount: number) => { - callback({ - more: more, - sqlError: sqlError, - attention: attention, - serverError: serverError, - rowCount: rowCountValid ? rowCount : undefined, - curCmd: curCmd - }); - }; + const more = !!(status & STATUS.MORE); + const sqlError = !!(status & STATUS.ERROR); + const rowCountValid = !!(status & STATUS.COUNT); + const attention = !!(status & STATUS.ATTN); + const serverError = !!(status & STATUS.SRVERROR); - if (options.tdsVersion < '7_2') { - parser.readUInt32LE(next); - } else { - parser.readBigUInt64LE((rowCount) => { - next(Number(rowCount)); - }); - } - }); - }); + let curCmd; + ({ offset, value: curCmd } = readUInt16LE(buf, offset)); + + let rowCount; + ({ offset, value: rowCount } = (options.tdsVersion < '7_2' ? readUInt32LE : readBigUInt64LE)(buf, offset)); + + return new Result({ + more: more, + sqlError: sqlError, + attention: attention, + serverError: serverError, + rowCount: rowCountValid ? Number(rowCount) : undefined, + curCmd: curCmd + }, offset); } -export function doneParser(parser: Parser, options: ParserOptions, callback: (token: DoneToken) => void) { - parseToken(parser, options, (data) => { - callback(new DoneToken(data)); - }); +export function doneParser(buf: Buffer, offset: number, options: ParserOptions): Result { + let value; + ({ offset, value } = readToken(buf, offset, options)); + return new Result(new DoneToken(value), offset); } -export function doneInProcParser(parser: Parser, options: ParserOptions, callback: (token: DoneInProcToken) => void) { - parseToken(parser, options, (data) => { - callback(new DoneInProcToken(data)); - }); +export function doneInProcParser(buf: Buffer, offset: number, options: ParserOptions): Result { + let value; + ({ offset, value } = readToken(buf, offset, options)); + return new Result(new DoneInProcToken(value), offset); } -export function doneProcParser(parser: Parser, options: ParserOptions, callback: (token: DoneProcToken) => void) { - parseToken(parser, options, (data) => { - callback(new DoneProcToken(data)); - }); +export function doneProcParser(buf: Buffer, offset: number, options: ParserOptions): Result { + let value; + ({ offset, value } = readToken(buf, offset, options)); + return new Result(new DoneProcToken(value), offset); } diff --git a/src/token/env-change-token-parser.ts b/src/token/env-change-token-parser.ts index 3f20a4012..d765a4f21 100644 --- a/src/token/env-change-token-parser.ts +++ b/src/token/env-change-token-parser.ts @@ -1,4 +1,4 @@ -import Parser, { type ParserOptions } from './stream-parser'; +import { type ParserOptions } from './stream-parser'; import { Collation } from '../collation'; import { @@ -12,21 +12,11 @@ import { DatabaseMirroringPartnerEnvChangeToken, ResetConnectionEnvChangeToken, RoutingEnvChangeToken, - CollationChangeToken + CollationChangeToken, + type EnvChangeToken } from './token'; -type EnvChangeToken = - DatabaseEnvChangeToken | - LanguageEnvChangeToken | - CharsetEnvChangeToken | - PacketSizeEnvChangeToken | - BeginTransactionEnvChangeToken | - CommitTransactionEnvChangeToken | - RollbackTransactionEnvChangeToken | - DatabaseMirroringPartnerEnvChangeToken | - ResetConnectionEnvChangeToken | - RoutingEnvChangeToken | - CollationChangeToken; +import { NotEnoughDataError, readBVarByte, readBVarChar, readUInt16LE, readUInt8, readUsVarByte, Result } from './helpers'; const types: { [key: number]: { name: string, event?: string }} = { 1: { @@ -78,124 +68,133 @@ const types: { [key: number]: { name: string, event?: string }} = { } }; -function readNewAndOldValue(parser: Parser, length: number, type: { name: string, event?: string }, callback: (token: EnvChangeToken | undefined) => void) { +function _readNewAndOldValue(buf: Buffer, offset: number, length: number, type: { name: string, event?: string }): Result { switch (type.name) { case 'DATABASE': case 'LANGUAGE': case 'CHARSET': case 'PACKET_SIZE': - case 'DATABASE_MIRRORING_PARTNER': - return parser.readBVarChar((newValue) => { - parser.readBVarChar((oldValue) => { - switch (type.name) { - case 'PACKET_SIZE': - return callback(new PacketSizeEnvChangeToken(parseInt(newValue), parseInt(oldValue))); + case 'DATABASE_MIRRORING_PARTNER': { + let newValue; + ({ offset, value: newValue } = readBVarChar(buf, offset)); - case 'DATABASE': - return callback(new DatabaseEnvChangeToken(newValue, oldValue)); + let oldValue; + ({ offset, value: oldValue } = readBVarChar(buf, offset)); - case 'LANGUAGE': - return callback(new LanguageEnvChangeToken(newValue, oldValue)); + switch (type.name) { + case 'PACKET_SIZE': + return new Result(new PacketSizeEnvChangeToken(parseInt(newValue), parseInt(oldValue)), offset); - case 'CHARSET': - return callback(new CharsetEnvChangeToken(newValue, oldValue)); + case 'DATABASE': + return new Result(new DatabaseEnvChangeToken(newValue, oldValue), offset); - case 'DATABASE_MIRRORING_PARTNER': - return callback(new DatabaseMirroringPartnerEnvChangeToken(newValue, oldValue)); - } - }); - }); + case 'LANGUAGE': + return new Result(new LanguageEnvChangeToken(newValue, oldValue), offset); + + case 'CHARSET': + return new Result(new CharsetEnvChangeToken(newValue, oldValue), offset); + + case 'DATABASE_MIRRORING_PARTNER': + return new Result(new DatabaseMirroringPartnerEnvChangeToken(newValue, oldValue), offset); + } + + throw new Error('unreachable'); + } case 'SQL_COLLATION': case 'BEGIN_TXN': case 'COMMIT_TXN': case 'ROLLBACK_TXN': - case 'RESET_CONNECTION': - return parser.readBVarByte((newValue) => { - parser.readBVarByte((oldValue) => { - switch (type.name) { - case 'SQL_COLLATION': { - const newCollation = newValue.length ? Collation.fromBuffer(newValue) : undefined; - const oldCollation = oldValue.length ? Collation.fromBuffer(oldValue) : undefined; - - return callback(new CollationChangeToken(newCollation, oldCollation)); - } - - case 'BEGIN_TXN': - return callback(new BeginTransactionEnvChangeToken(newValue, oldValue)); - - case 'COMMIT_TXN': - return callback(new CommitTransactionEnvChangeToken(newValue, oldValue)); - - case 'ROLLBACK_TXN': - return callback(new RollbackTransactionEnvChangeToken(newValue, oldValue)); - - case 'RESET_CONNECTION': - return callback(new ResetConnectionEnvChangeToken(newValue, oldValue)); - } - }); - }); - - case 'ROUTING_CHANGE': - return parser.readUInt16LE((valueLength) => { - // Routing Change: - // Byte 1: Protocol (must be 0) - // Bytes 2-3 (USHORT): Port number - // Bytes 4-5 (USHORT): Length of server data in unicode (2byte chars) - // Bytes 6-*: Server name in unicode characters - parser.readBuffer(valueLength, (routePacket) => { - const protocol = routePacket.readUInt8(0); - - if (protocol !== 0) { - throw new Error('Unknown protocol byte in routing change event'); - } - - const port = routePacket.readUInt16LE(1); - const serverLen = routePacket.readUInt16LE(3); - // 2 bytes per char, starting at offset 5 - const server = routePacket.toString('ucs2', 5, 5 + (serverLen * 2)); - - const newValue = { - protocol: protocol, - port: port, - server: server - }; - - parser.readUInt16LE((oldValueLength) => { - parser.readBuffer(oldValueLength, (oldValue) => { - callback(new RoutingEnvChangeToken(newValue, oldValue)); - }); - }); - }); - }); - - default: + case 'RESET_CONNECTION': { + let newValue; + ({ offset, value: newValue } = readBVarByte(buf, offset)); + + let oldValue; + ({ offset, value: oldValue } = readBVarByte(buf, offset)); + + switch (type.name) { + case 'SQL_COLLATION': { + const newCollation = newValue.length ? Collation.fromBuffer(newValue) : undefined; + const oldCollation = oldValue.length ? Collation.fromBuffer(oldValue) : undefined; + + return new Result(new CollationChangeToken(newCollation, oldCollation), offset); + } + + case 'BEGIN_TXN': + return new Result(new BeginTransactionEnvChangeToken(newValue, oldValue), offset); + + case 'COMMIT_TXN': + return new Result(new CommitTransactionEnvChangeToken(newValue, oldValue), offset); + + case 'ROLLBACK_TXN': + return new Result(new RollbackTransactionEnvChangeToken(newValue, oldValue), offset); + + case 'RESET_CONNECTION': + return new Result(new ResetConnectionEnvChangeToken(newValue, oldValue), offset); + } + + throw new Error('unreachable'); + } + + case 'ROUTING_CHANGE': { + let routePacket; + ({ offset, value: routePacket } = readUsVarByte(buf, offset)); + + let oldValue; + ({ offset, value: oldValue } = readUsVarByte(buf, offset)); + + // Routing Change: + // Byte 1: Protocol (must be 0) + // Bytes 2-3 (USHORT): Port number + // Bytes 4-5 (USHORT): Length of server data in unicode (2byte chars) + // Bytes 6-*: Server name in unicode characters + const protocol = routePacket.readUInt8(0); + if (protocol !== 0) { + throw new Error('Unknown protocol byte in routing change event'); + } + + const port = routePacket.readUInt16LE(1); + const serverLen = routePacket.readUInt16LE(3); + // 2 bytes per char, starting at offset 5 + const server = routePacket.toString('ucs2', 5, 5 + (serverLen * 2)); + + const newValue = { + protocol: protocol, + port: port, + server: server + }; + + return new Result(new RoutingEnvChangeToken(newValue, oldValue), offset); + } + + default: { console.error('Tedious > Unsupported ENVCHANGE type ' + type.name); + // skip unknown bytes - parser.readBuffer(length - 1, () => { - callback(undefined); - }); + return new Result(undefined, offset + length - 1); + } } } -function envChangeParser(parser: Parser, _options: ParserOptions, callback: (token: EnvChangeToken | undefined) => void) { - parser.readUInt16LE((length) => { - parser.readUInt8((typeNumber) => { - const type = types[typeNumber]; - - if (!type) { - console.error('Tedious > Unsupported ENVCHANGE type ' + typeNumber); - // skip unknown bytes - return parser.readBuffer(length - 1, () => { - callback(undefined); - }); - } +function envChangeParser(buf: Buffer, offset: number, _options: ParserOptions): Result { + let tokenLength; + ({ offset, value: tokenLength } = readUInt16LE(buf, offset)); + + if (buf.length < offset + tokenLength) { + throw new NotEnoughDataError(offset + tokenLength); + } + + let typeNumber; + ({ offset, value: typeNumber } = readUInt8(buf, offset)); + + const type = types[typeNumber]; + + if (!type) { + console.error('Tedious > Unsupported ENVCHANGE type ' + typeNumber); + return new Result(undefined, offset + tokenLength - 1); + } - readNewAndOldValue(parser, length, type, (token) => { - callback(token); - }); - }); - }); + return _readNewAndOldValue(buf, offset, tokenLength, type); } export default envChangeParser; diff --git a/src/token/feature-ext-ack-parser.ts b/src/token/feature-ext-ack-parser.ts index bb977c2b8..0e6a5d034 100644 --- a/src/token/feature-ext-ack-parser.ts +++ b/src/token/feature-ext-ack-parser.ts @@ -1,4 +1,5 @@ -import Parser, { type ParserOptions } from './stream-parser'; +import { NotEnoughDataError, readUInt32LE, readUInt8, Result } from './helpers'; +import { type ParserOptions } from './stream-parser'; import { FeatureExtAckToken } from './token'; @@ -12,33 +13,37 @@ const FEATURE_ID = { TERMINATOR: 0xFF }; -function featureExtAckParser(parser: Parser, _options: ParserOptions, callback: (token: FeatureExtAckToken) => void) { +function featureExtAckParser(buf: Buffer, offset: number, _options: ParserOptions): Result { let fedAuth: Buffer | undefined; let utf8Support: boolean | undefined; - function next() { - parser.readUInt8((featureId) => { - if (featureId === FEATURE_ID.TERMINATOR) { - return callback(new FeatureExtAckToken(fedAuth, utf8Support)); - } - - parser.readUInt32LE((featureAckDataLen) => { - parser.readBuffer(featureAckDataLen, (featureData) => { - switch (featureId) { - case FEATURE_ID.FEDAUTH: - fedAuth = featureData; - break; - case FEATURE_ID.UTF8_SUPPORT: - utf8Support = !!featureData[0]; - break; - } - next(); - }); - }); - }); - } + while (true) { + let featureId; + ({ value: featureId, offset } = readUInt8(buf, offset)); + + if (featureId === FEATURE_ID.TERMINATOR) { + return new Result(new FeatureExtAckToken(fedAuth, utf8Support), offset); + } + + let featureAckDataLen; + ({ value: featureAckDataLen, offset } = readUInt32LE(buf, offset)); - next(); + if (buf.length < offset + featureAckDataLen) { + throw new NotEnoughDataError(offset + featureAckDataLen); + } + + const featureData = buf.slice(offset, offset + featureAckDataLen); + offset += featureAckDataLen; + + switch (featureId) { + case FEATURE_ID.FEDAUTH: + fedAuth = featureData; + break; + case FEATURE_ID.UTF8_SUPPORT: + utf8Support = !!featureData[0]; + break; + } + } } export default featureExtAckParser; diff --git a/src/token/fedauth-info-parser.ts b/src/token/fedauth-info-parser.ts index e4bcd8c8e..47404b8e4 100644 --- a/src/token/fedauth-info-parser.ts +++ b/src/token/fedauth-info-parser.ts @@ -1,4 +1,5 @@ -import Parser, { type ParserOptions } from './stream-parser'; +import { NotEnoughDataError, readUInt32LE, Result } from './helpers'; +import { type ParserOptions } from './stream-parser'; import { FedAuthInfoToken } from './token'; const FEDAUTHINFOID = { @@ -6,44 +7,54 @@ const FEDAUTHINFOID = { SPN: 0x02 }; -function fedAuthInfoParser(parser: Parser, _options: ParserOptions, callback: (token: FedAuthInfoToken) => void) { - parser.readUInt32LE((tokenLength) => { - parser.readBuffer(tokenLength, (data) => { - let spn: string | undefined, stsurl: string | undefined; +function readFedAuthInfo(data: Buffer): { spn: string | undefined, stsurl: string | undefined } { + let offset = 0; + let spn, stsurl; - let offset = 0; + const countOfInfoIDs = data.readUInt32LE(offset); + offset += 4; - const countOfInfoIDs = data.readUInt32LE(offset); - offset += 4; + for (let i = 0; i < countOfInfoIDs; i++) { + const fedauthInfoID = data.readUInt8(offset); + offset += 1; - for (let i = 0; i < countOfInfoIDs; i++) { - const fedauthInfoID = data.readUInt8(offset); - offset += 1; + const fedAuthInfoDataLen = data.readUInt32LE(offset); + offset += 4; - const fedAuthInfoDataLen = data.readUInt32LE(offset); - offset += 4; + const fedAuthInfoDataOffset = data.readUInt32LE(offset); + offset += 4; - const fedAuthInfoDataOffset = data.readUInt32LE(offset); - offset += 4; + switch (fedauthInfoID) { + case FEDAUTHINFOID.SPN: + spn = data.toString('ucs2', fedAuthInfoDataOffset, fedAuthInfoDataOffset + fedAuthInfoDataLen); + break; - switch (fedauthInfoID) { - case FEDAUTHINFOID.SPN: - spn = data.toString('ucs2', fedAuthInfoDataOffset, fedAuthInfoDataOffset + fedAuthInfoDataLen); - break; + case FEDAUTHINFOID.STSURL: + stsurl = data.toString('ucs2', fedAuthInfoDataOffset, fedAuthInfoDataOffset + fedAuthInfoDataLen); + break; - case FEDAUTHINFOID.STSURL: - stsurl = data.toString('ucs2', fedAuthInfoDataOffset, fedAuthInfoDataOffset + fedAuthInfoDataLen); - break; + // ignoring unknown fedauthinfo options + default: + break; + } + } - // ignoring unknown fedauthinfo options - default: - break; - } - } + return { spn, stsurl }; +} + +function fedAuthInfoParser(buf: Buffer, offset: number, _options: ParserOptions): Result { + let tokenLength; + ({ offset, value: tokenLength } = readUInt32LE(buf, offset)); + + if (buf.length < offset + tokenLength) { + throw new NotEnoughDataError(offset + tokenLength); + } + + const data = buf.slice(offset, offset + tokenLength); + offset += tokenLength; - callback(new FedAuthInfoToken(spn, stsurl)); - }); - }); + const { spn, stsurl } = readFedAuthInfo(data); + return new Result(new FedAuthInfoToken(spn, stsurl), offset); } export default fedAuthInfoParser; diff --git a/src/token/helpers.ts b/src/token/helpers.ts new file mode 100644 index 000000000..c5a1d7137 --- /dev/null +++ b/src/token/helpers.ts @@ -0,0 +1,236 @@ +export class Result { + declare value: T; + declare offset: number; + + constructor(value: T, offset: number) { + this.value = value; + this.offset = offset; + } +} + +export class NotEnoughDataError extends Error { + byteCount: number; + + constructor(byteCount: number) { + super(); + + this.byteCount = byteCount; + } +} + +export function readUInt8(buf: Buffer, offset: number): Result { + offset = +offset; + + if (buf.length < offset + 1) { + throw new NotEnoughDataError(offset + 1); + } + + return new Result(buf.readUInt8(offset), offset + 1); +} + +export function readUInt16LE(buf: Buffer, offset: number): Result { + offset = +offset; + + if (buf.length < offset + 2) { + throw new NotEnoughDataError(offset + 2); + } + + return new Result(buf.readUInt16LE(offset), offset + 2); +} + +export function readInt16LE(buf: Buffer, offset: number): Result { + offset = +offset; + + if (buf.length < offset + 2) { + throw new NotEnoughDataError(offset + 2); + } + + return new Result(buf.readInt16LE(offset), offset + 2); +} + +export function readUInt24LE(buf: Buffer, offset: number): Result { + offset = +offset; + + if (buf.length < offset + 3) { + throw new NotEnoughDataError(offset + 3); + } + + return new Result(buf.readUIntLE(offset, 3), offset + 3); +} + +export function readUInt32LE(buf: Buffer, offset: number): Result { + offset = +offset; + + if (buf.length < offset + 4) { + throw new NotEnoughDataError(offset + 4); + } + + return new Result(buf.readUInt32LE(offset), offset + 4); +} + +export function readUInt32BE(buf: Buffer, offset: number): Result { + offset = +offset; + + if (buf.length < offset + 4) { + throw new NotEnoughDataError(offset + 4); + } + + return new Result(buf.readUInt32BE(offset), offset + 4); +} + +export function readUInt40LE(buf: Buffer, offset: number): Result { + offset = +offset; + + if (buf.length < offset + 5) { + throw new NotEnoughDataError(offset + 5); + } + + return new Result(buf.readUIntLE(offset, 5), offset + 5); +} +export function readInt32LE(buf: Buffer, offset: number): Result { + offset = +offset; + + if (buf.length < offset + 4) { + throw new NotEnoughDataError(offset + 4); + } + + return new Result(buf.readInt32LE(offset), offset + 4); +} + +export function readBigUInt64LE(buf: Buffer, offset: number): Result { + offset = +offset; + + if (buf.length < offset + 8) { + throw new NotEnoughDataError(offset + 8); + } + + return new Result(buf.readBigUInt64LE(offset), offset + 8); +} + +export function readBigInt64LE(buf: Buffer, offset: number): Result { + offset = +offset; + + if (buf.length < offset + 8) { + throw new NotEnoughDataError(offset + 8); + } + + return new Result(buf.readBigInt64LE(offset), offset + 8); +} + +export function readFloatLE(buf: Buffer, offset: number): Result { + offset = +offset; + + if (buf.length < offset + 4) { + throw new NotEnoughDataError(offset + 4); + } + + return new Result(buf.readFloatLE(offset), offset + 4); +} + +export function readDoubleLE(buf: Buffer, offset: number): Result { + offset = +offset; + + if (buf.length < offset + 8) { + throw new NotEnoughDataError(offset + 8); + } + + return new Result(buf.readDoubleLE(offset), offset + 8); +} + +export function readBVarChar(buf: Buffer, offset: number): Result { + offset = +offset; + + let charCount; + ({ offset, value: charCount } = readUInt8(buf, offset)); + + const byteLength = charCount * 2; + + if (buf.length < offset + byteLength) { + throw new NotEnoughDataError(offset + byteLength); + } + + return new Result(buf.toString('ucs2', offset, offset + byteLength), offset + byteLength); +} + +export function readBVarByte(buf: Buffer, offset: number): Result { + offset = +offset; + + let byteLength; + ({ offset, value: byteLength } = readUInt8(buf, offset)); + + if (buf.length < offset + byteLength) { + throw new NotEnoughDataError(offset + byteLength); + } + + return new Result(buf.slice(offset, offset + byteLength), offset + byteLength); +} + +export function readUsVarChar(buf: Buffer, offset: number): Result { + offset = +offset; + + let charCount; + ({ offset, value: charCount } = readUInt16LE(buf, offset)); + + const byteLength = charCount * 2; + + if (buf.length < offset + byteLength) { + throw new NotEnoughDataError(offset + byteLength); + } + + return new Result(buf.toString('ucs2', offset, offset + byteLength), offset + byteLength); +} + +export function readUsVarByte(buf: Buffer, offset: number): Result { + offset = +offset; + + let byteLength; + ({ offset, value: byteLength } = readUInt16LE(buf, offset)); + + if (buf.length < offset + byteLength) { + throw new NotEnoughDataError(offset + byteLength); + } + + return new Result(buf.slice(offset, offset + byteLength), offset + byteLength); +} + +export function readUNumeric64LE(buf: Buffer, offset: number): Result { + offset = +offset; + + if (buf.length < offset + 8) { + throw new NotEnoughDataError(offset + 8); + } + + const low = buf.readUInt32LE(offset); + const high = buf.readUInt32LE(offset + 4); + + return new Result((0x100000000 * high) + low, offset + 8); +} + +export function readUNumeric96LE(buf: Buffer, offset: number): Result { + offset = +offset; + + if (buf.length < offset + 12) { + throw new NotEnoughDataError(offset + 12); + } + + const dword1 = buf.readUInt32LE(offset); + const dword2 = buf.readUInt32LE(offset + 4); + const dword3 = buf.readUInt32LE(offset + 8); + + return new Result(dword1 + (0x100000000 * dword2) + (0x100000000 * 0x100000000 * dword3), offset + 12); +} + +export function readUNumeric128LE(buf: Buffer, offset: number): Result { + offset = +offset; + + if (buf.length < offset + 16) { + throw new NotEnoughDataError(offset + 16); + } + + const dword1 = buf.readUInt32LE(offset); + const dword2 = buf.readUInt32LE(offset + 4); + const dword3 = buf.readUInt32LE(offset + 8); + const dword4 = buf.readUInt32LE(offset + 12); + + return new Result(dword1 + (0x100000000 * dword2) + (0x100000000 * 0x100000000 * dword3) + (0x100000000 * 0x100000000 * 0x100000000 * dword4), offset + 16); +} diff --git a/src/token/infoerror-token-parser.ts b/src/token/infoerror-token-parser.ts index 1c38d427f..328adb761 100644 --- a/src/token/infoerror-token-parser.ts +++ b/src/token/infoerror-token-parser.ts @@ -1,4 +1,5 @@ -import Parser, { type ParserOptions } from './stream-parser'; +import { NotEnoughDataError, readBVarChar, readUInt16LE, readUInt32LE, readUInt8, readUsVarChar, Result } from './helpers'; +import { type ParserOptions } from './stream-parser'; import { InfoMessageToken, ErrorMessageToken } from './token'; @@ -12,43 +13,56 @@ interface TokenData { lineNumber: number; } -function parseToken(parser: Parser, options: ParserOptions, callback: (data: TokenData) => void) { - // length - parser.readUInt16LE(() => { - parser.readUInt32LE((number) => { - parser.readUInt8((state) => { - parser.readUInt8((clazz) => { - parser.readUsVarChar((message) => { - parser.readBVarChar((serverName) => { - parser.readBVarChar((procName) => { - (options.tdsVersion < '7_2' ? parser.readUInt16LE : parser.readUInt32LE).call(parser, (lineNumber: number) => { - callback({ - 'number': number, - 'state': state, - 'class': clazz, - 'message': message, - 'serverName': serverName, - 'procName': procName, - 'lineNumber': lineNumber - }); - }); - }); - }); - }); - }); - }); - }); - }); +function readToken(buf: Buffer, offset: number, options: ParserOptions): Result { + let tokenLength; + ({ offset, value: tokenLength } = readUInt16LE(buf, offset)); + + if (buf.length < tokenLength + offset) { + throw new NotEnoughDataError(tokenLength + offset); + } + + let number; + ({ offset, value: number } = readUInt32LE(buf, offset)); + + let state; + ({ offset, value: state } = readUInt8(buf, offset)); + + let clazz; + ({ offset, value: clazz } = readUInt8(buf, offset)); + + let message; + ({ offset, value: message } = readUsVarChar(buf, offset)); + + let serverName; + ({ offset, value: serverName } = readBVarChar(buf, offset)); + + let procName; + ({ offset, value: procName } = readBVarChar(buf, offset)); + + let lineNumber; + ({ offset, value: lineNumber } = options.tdsVersion < '7_2' ? readUInt16LE(buf, offset) : readUInt32LE(buf, offset)); + + return new Result({ + 'number': number, + 'state': state, + 'class': clazz, + 'message': message, + 'serverName': serverName, + 'procName': procName, + 'lineNumber': lineNumber + }, offset); } -export function infoParser(parser: Parser, options: ParserOptions, callback: (token: InfoMessageToken) => void) { - parseToken(parser, options, (data) => { - callback(new InfoMessageToken(data)); - }); +export function infoParser(buf: Buffer, offset: number, options: ParserOptions): Result { + let data; + ({ offset, value: data } = readToken(buf, offset, options)); + + return new Result(new InfoMessageToken(data), offset); } -export function errorParser(parser: Parser, options: ParserOptions, callback: (token: ErrorMessageToken) => void) { - parseToken(parser, options, (data) => { - callback(new ErrorMessageToken(data)); - }); +export function errorParser(buf: Buffer, offset: number, options: ParserOptions): Result { + let data; + ({ offset, value: data } = readToken(buf, offset, options)); + + return new Result(new ErrorMessageToken(data), offset); } diff --git a/src/token/loginack-token-parser.ts b/src/token/loginack-token-parser.ts index 4abda8d45..35dcc6a65 100644 --- a/src/token/loginack-token-parser.ts +++ b/src/token/loginack-token-parser.ts @@ -1,45 +1,60 @@ -import Parser, { type ParserOptions } from './stream-parser'; +import { type ParserOptions } from './stream-parser'; import { LoginAckToken } from './token'; import { versionsByValue as versions } from '../tds-versions'; +import { NotEnoughDataError, readBVarChar, readUInt16LE, readUInt32BE, readUInt8, Result } from './helpers'; const interfaceTypes: { [key: number]: string } = { 0: 'SQL_DFLT', 1: 'SQL_TSQL' }; -function loginAckParser(parser: Parser, _options: ParserOptions, callback: (token: LoginAckToken) => void) { +function loginAckParser(buf: Buffer, offset: number, _options: ParserOptions): Result { // length - parser.readUInt16LE(() => { - parser.readUInt8((interfaceNumber) => { - const interfaceType = interfaceTypes[interfaceNumber]; - parser.readUInt32BE((tdsVersionNumber) => { - const tdsVersion = versions[tdsVersionNumber]; - parser.readBVarChar((progName) => { - parser.readUInt8((major) => { - parser.readUInt8((minor) => { - parser.readUInt8((buildNumHi) => { - parser.readUInt8((buildNumLow) => { - callback(new LoginAckToken({ - interface: interfaceType, - tdsVersion: tdsVersion, - progName: progName, - progVersion: { - major: major, - minor: minor, - buildNumHi: buildNumHi, - buildNumLow: buildNumLow - } - })); - }); - }); - }); - }); - }); - }); - }); - }); + let tokenLength; + ({ offset, value: tokenLength } = readUInt16LE(buf, offset)); + + if (buf.length < tokenLength + offset) { + throw new NotEnoughDataError(tokenLength + offset); + } + + let interfaceNumber; + ({ offset, value: interfaceNumber } = readUInt8(buf, offset)); + + const interfaceType = interfaceTypes[interfaceNumber]; + + let tdsVersionNumber; + ({ offset, value: tdsVersionNumber } = readUInt32BE(buf, offset)); + + const tdsVersion = versions[tdsVersionNumber]; + + let progName; + ({ offset, value: progName } = readBVarChar(buf, offset)); + + let major; + ({ offset, value: major } = readUInt8(buf, offset)); + + let minor; + ({ offset, value: minor } = readUInt8(buf, offset)); + + let buildNumHi; + ({ offset, value: buildNumHi } = readUInt8(buf, offset)); + + let buildNumLow; + ({ offset, value: buildNumLow } = readUInt8(buf, offset)); + + return new Result(new LoginAckToken({ + interface: interfaceType, + tdsVersion: tdsVersion, + progName: progName, + progVersion: { + major: major, + minor: minor, + buildNumHi: buildNumHi, + buildNumLow: buildNumLow + } + }), offset); } export default loginAckParser; diff --git a/src/token/nbcrow-token-parser.ts b/src/token/nbcrow-token-parser.ts index 4a7012ed7..0b8d07468 100644 --- a/src/token/nbcrow-token-parser.ts +++ b/src/token/nbcrow-token-parser.ts @@ -1,15 +1,13 @@ // s2.2.7.13 (introduced in TDS 7.3.B) -import Parser, { type ParserOptions } from './stream-parser'; +import Parser from './stream-parser'; import { type ColumnMetadata } from './colmetadata-token-parser'; import { NBCRowToken } from './token'; +import * as iconv from 'iconv-lite'; -import valueParse from '../value-parser'; - -function nullHandler(_parser: Parser, _columnMetadata: ColumnMetadata, _options: ParserOptions, callback: (value: unknown) => void) { - callback(null); -} +import { isPLPStream, readPLPStream, readValue } from '../value-parser'; +import { NotEnoughDataError } from './helpers'; interface Column { value: unknown; @@ -18,12 +16,12 @@ interface Column { async function nbcRowParser(parser: Parser): Promise { const colMetadata = parser.colMetadata; - const bitmapByteLength = Math.ceil(colMetadata.length / 8); const columns: Column[] = []; const bitmap: boolean[] = []; + const bitmapByteLength = Math.ceil(colMetadata.length / 8); while (parser.buffer.length - parser.position < bitmapByteLength) { - await parser.streamBuffer.waitForChunk(); + await parser.waitForChunk(); } const bytes = parser.buffer.slice(parser.position, parser.position + bitmapByteLength); @@ -43,28 +41,48 @@ async function nbcRowParser(parser: Parser): Promise { } for (let i = 0; i < colMetadata.length; i++) { - const currColMetadata = colMetadata[i]; - let value; - (bitmap[i] ? nullHandler : valueParse)(parser, currColMetadata, parser.options, (v) => { - value = v; - }); - - while (parser.suspended) { - await parser.streamBuffer.waitForChunk(); + const metadata = colMetadata[i]; + if (bitmap[i]) { + columns.push({ value: null, metadata }); + continue; + } - parser.suspended = false; - const next = parser.next!; + while (true) { + if (isPLPStream(metadata)) { + const chunks = await readPLPStream(parser); + + if (chunks === null) { + columns.push({ value: chunks, metadata }); + } else if (metadata.type.name === 'NVarChar' || metadata.type.name === 'Xml') { + columns.push({ value: Buffer.concat(chunks).toString('ucs2'), metadata }); + } else if (metadata.type.name === 'VarChar') { + columns.push({ value: iconv.decode(Buffer.concat(chunks), metadata.collation?.codepage ?? 'utf8'), metadata }); + } else if (metadata.type.name === 'VarBinary' || metadata.type.name === 'UDT') { + columns.push({ value: Buffer.concat(chunks), metadata }); + } + } else { + let result; + try { + result = readValue(parser.buffer, parser.position, metadata, parser.options); + } catch (err) { + if (err instanceof NotEnoughDataError) { + await parser.waitForChunk(); + continue; + } + + throw err; + } + + parser.position = result.offset; + columns.push({ value: result.value, metadata }); + } - next(); + break; } - columns.push({ - value, - metadata: currColMetadata - }); } if (parser.options.useColumnNames) { - const columnsMap: { [key: string]: Column } = {}; + const columnsMap: { [key: string]: Column } = Object.create(null); columns.forEach((column) => { const colName = column.metadata.colName; @@ -79,5 +97,6 @@ async function nbcRowParser(parser: Parser): Promise { } } + export default nbcRowParser; module.exports = nbcRowParser; diff --git a/src/token/order-token-parser.ts b/src/token/order-token-parser.ts index bcc3498cb..0a09f5ddf 100644 --- a/src/token/order-token-parser.ts +++ b/src/token/order-token-parser.ts @@ -1,32 +1,28 @@ // s2.2.7.14 -import Parser, { type ParserOptions } from './stream-parser'; +import { type ParserOptions } from './stream-parser'; import { OrderToken } from './token'; +import { NotEnoughDataError, readUInt16LE, Result } from './helpers'; -function orderParser(parser: Parser, _options: ParserOptions, callback: (token: OrderToken) => void) { - parser.readUInt16LE((length) => { - const columnCount = length / 2; - const orderColumns: number[] = []; +function orderParser(buf: Buffer, offset: number, _options: ParserOptions): Result { + // length + let tokenLength; + ({ offset, value: tokenLength } = readUInt16LE(buf, offset)); - let i = 0; - function next(done: () => void) { - if (i === columnCount) { - return done(); - } + if (buf.length < offset + tokenLength) { + throw new NotEnoughDataError(offset + tokenLength); + } - parser.readUInt16LE((column) => { - orderColumns.push(column); + const orderColumns: number[] = []; - i++; + for (let i = 0; i < tokenLength; i += 2) { + let column; + ({ offset, value: column } = readUInt16LE(buf, offset)); - next(done); - }); - } + orderColumns.push(column); + } - next(() => { - callback(new OrderToken(orderColumns)); - }); - }); + return new Result(new OrderToken(orderColumns), offset); } export default orderParser; diff --git a/src/token/returnstatus-token-parser.ts b/src/token/returnstatus-token-parser.ts index 83be29b7d..1ae4b1a34 100644 --- a/src/token/returnstatus-token-parser.ts +++ b/src/token/returnstatus-token-parser.ts @@ -1,12 +1,13 @@ // s2.2.7.16 -import Parser, { type ParserOptions } from './stream-parser'; +import { readInt32LE, Result } from './helpers'; +import { type ParserOptions } from './stream-parser'; import { ReturnStatusToken } from './token'; -function returnStatusParser(parser: Parser, _options: ParserOptions, callback: (token: ReturnStatusToken) => void) { - parser.readInt32LE((value) => { - callback(new ReturnStatusToken(value)); - }); +function returnStatusParser(buf: Buffer, offset: number, _options: ParserOptions): Result { + let value; + ({ value, offset } = readInt32LE(buf, offset)); + return new Result(new ReturnStatusToken(value), offset); } export default returnStatusParser; diff --git a/src/token/returnvalue-token-parser.ts b/src/token/returnvalue-token-parser.ts index 64b195fdc..bcfa14ced 100644 --- a/src/token/returnvalue-token-parser.ts +++ b/src/token/returnvalue-token-parser.ts @@ -1,33 +1,86 @@ // s2.2.7.16 -import Parser, { type ParserOptions } from './stream-parser'; +import Parser from './stream-parser'; import { ReturnValueToken } from './token'; -import metadataParse from '../metadata-parser'; -import valueParse from '../value-parser'; +import { readMetadata } from '../metadata-parser'; +import { isPLPStream, readPLPStream, readValue } from '../value-parser'; +import { NotEnoughDataError, readBVarChar, readUInt16LE, readUInt8 } from './helpers'; +import * as iconv from 'iconv-lite'; + +async function returnParser(parser: Parser): Promise { + let paramName; + let paramOrdinal; + let metadata; + + while (true) { + const buf = parser.buffer; + let offset = parser.position; + + try { + ({ offset, value: paramOrdinal } = readUInt16LE(buf, offset)); + ({ offset, value: paramName } = readBVarChar(buf, offset)); + // status + ({ offset } = readUInt8(buf, offset)); + ({ offset, value: metadata } = readMetadata(buf, offset, parser.options)); -function returnParser(parser: Parser, options: ParserOptions, callback: (token: ReturnValueToken) => void) { - parser.readUInt16LE((paramOrdinal) => { - parser.readBVarChar((paramName) => { if (paramName.charAt(0) === '@') { paramName = paramName.slice(1); } + } catch (err) { + if (err instanceof NotEnoughDataError) { + await parser.waitForChunk(); + continue; + } - // status - parser.readUInt8(() => { - metadataParse(parser, options, (metadata) => { - valueParse(parser, metadata, options, (value) => { - callback(new ReturnValueToken({ - paramOrdinal: paramOrdinal, - paramName: paramName, - metadata: metadata, - value: value - })); - }); - }); - }); - }); + throw err; + } + + parser.position = offset; + break; + } + + let value; + while (true) { + const buf = parser.buffer; + let offset = parser.position; + + if (isPLPStream(metadata)) { + const chunks = await readPLPStream(parser); + + if (chunks === null) { + value = chunks; + } else if (metadata.type.name === 'NVarChar' || metadata.type.name === 'Xml') { + value = Buffer.concat(chunks).toString('ucs2'); + } else if (metadata.type.name === 'VarChar') { + value = iconv.decode(Buffer.concat(chunks), metadata.collation?.codepage ?? 'utf8'); + } else if (metadata.type.name === 'VarBinary' || metadata.type.name === 'UDT') { + value = Buffer.concat(chunks); + } + } else { + try { + ({ value, offset } = readValue(buf, offset, metadata, parser.options)); + } catch (err) { + if (err instanceof NotEnoughDataError) { + await parser.waitForChunk(); + continue; + } + + throw err; + } + + parser.position = offset; + } + + break; + } + + return new ReturnValueToken({ + paramOrdinal: paramOrdinal, + paramName: paramName, + metadata: metadata, + value: value }); } diff --git a/src/token/row-token-parser.ts b/src/token/row-token-parser.ts index 7960bc3b3..f394dbf97 100644 --- a/src/token/row-token-parser.ts +++ b/src/token/row-token-parser.ts @@ -4,8 +4,10 @@ import Parser from './stream-parser'; import { type ColumnMetadata } from './colmetadata-token-parser'; import { RowToken } from './token'; +import * as iconv from 'iconv-lite'; -import valueParse from '../value-parser'; +import { isPLPStream, readPLPStream, readValue } from '../value-parser'; +import { NotEnoughDataError } from './helpers'; interface Column { value: unknown; @@ -13,29 +15,41 @@ interface Column { } async function rowParser(parser: Parser): Promise { - const colMetadata = parser.colMetadata; - const length = colMetadata.length; const columns: Column[] = []; - for (let i = 0; i < length; i++) { - const currColMetadata = colMetadata[i]; - let value; - valueParse(parser, currColMetadata, parser.options, (v) => { - value = v; - }); + for (const metadata of parser.colMetadata) { + while (true) { + if (isPLPStream(metadata)) { + const chunks = await readPLPStream(parser); + + if (chunks === null) { + columns.push({ value: chunks, metadata }); + } else if (metadata.type.name === 'NVarChar' || metadata.type.name === 'Xml') { + columns.push({ value: Buffer.concat(chunks).toString('ucs2'), metadata }); + } else if (metadata.type.name === 'VarChar') { + columns.push({ value: iconv.decode(Buffer.concat(chunks), metadata.collation?.codepage ?? 'utf8'), metadata }); + } else if (metadata.type.name === 'VarBinary' || metadata.type.name === 'UDT') { + columns.push({ value: Buffer.concat(chunks), metadata }); + } + } else { + let result; + try { + result = readValue(parser.buffer, parser.position, metadata, parser.options); + } catch (err) { + if (err instanceof NotEnoughDataError) { + await parser.waitForChunk(); + continue; + } - while (parser.suspended) { - await parser.streamBuffer.waitForChunk(); + throw err; + } - parser.suspended = false; - const next = parser.next!; + parser.position = result.offset; + columns.push({ value: result.value, metadata }); + } - next(); + break; } - columns.push({ - value, - metadata: currColMetadata - }); } if (parser.options.useColumnNames) { diff --git a/src/token/sspi-token-parser.ts b/src/token/sspi-token-parser.ts index ef91c6f57..0f4b09711 100644 --- a/src/token/sspi-token-parser.ts +++ b/src/token/sspi-token-parser.ts @@ -1,4 +1,5 @@ -import Parser, { type ParserOptions } from './stream-parser'; +import { NotEnoughDataError, readUInt16LE, Result } from './helpers'; +import { type ParserOptions } from './stream-parser'; import { SSPIToken } from './token'; @@ -40,10 +41,18 @@ function parseChallenge(buffer: Buffer) { return challenge as Data; } -function sspiParser(parser: Parser, _options: ParserOptions, callback: (token: SSPIToken) => void) { - parser.readUsVarByte((buffer) => { - callback(new SSPIToken(parseChallenge(buffer), buffer)); - }); +function sspiParser(buf: Buffer, offset: number, _options: ParserOptions): Result { + let tokenLength; + ({ offset, value: tokenLength } = readUInt16LE(buf, offset)); + + if (buf.length < offset + tokenLength) { + throw new NotEnoughDataError(offset + tokenLength); + } + + const data = buf.slice(offset, offset + tokenLength); + offset += tokenLength; + + return new Result(new SSPIToken(parseChallenge(data), data), offset); } export default sspiParser; diff --git a/src/token/stream-parser.ts b/src/token/stream-parser.ts index 1e7deaa84..abcca5674 100644 --- a/src/token/stream-parser.ts +++ b/src/token/stream-parser.ts @@ -1,7 +1,7 @@ import Debug from '../debug'; import { type InternalConnectionOptions } from '../connection'; -import { TYPE, Token, ColMetadataToken } from './token'; +import { TYPE, ColMetadataToken, DoneProcToken, DoneToken, DoneInProcToken, ErrorMessageToken, InfoMessageToken, RowToken, type EnvChangeToken, LoginAckToken, ReturnStatusToken, OrderToken, FedAuthInfoToken, SSPIToken, ReturnValueToken, NBCRowToken, FeatureExtAckToken, Token } from './token'; import colMetadataParser, { type ColumnMetadata } from './colmetadata-token-parser'; import { doneParser, doneInProcParser, doneProcParser } from './done-token-parser'; @@ -16,423 +16,388 @@ import returnValueParser from './returnvalue-token-parser'; import rowParser from './row-token-parser'; import nbcRowParser from './nbcrow-token-parser'; import sspiParser from './sspi-token-parser'; - -const tokenParsers = { - [TYPE.DONE]: doneParser, - [TYPE.DONEINPROC]: doneInProcParser, - [TYPE.DONEPROC]: doneProcParser, - [TYPE.ENVCHANGE]: envChangeParser, - [TYPE.ERROR]: errorParser, - [TYPE.FEDAUTHINFO]: fedAuthInfoParser, - [TYPE.FEATUREEXTACK]: featureExtAckParser, - [TYPE.INFO]: infoParser, - [TYPE.LOGINACK]: loginAckParser, - [TYPE.ORDER]: orderParser, - [TYPE.RETURNSTATUS]: returnStatusParser, - [TYPE.RETURNVALUE]: returnValueParser, - [TYPE.SSPI]: sspiParser -}; +import { NotEnoughDataError } from './helpers'; export type ParserOptions = Pick; -class StreamBuffer { - declare iterator: AsyncIterator | Iterator; - declare buffer: Buffer; - declare position: number; - - constructor(iterable: AsyncIterable | Iterable) { - this.iterator = ((iterable as AsyncIterable)[Symbol.asyncIterator] || (iterable as Iterable)[Symbol.iterator]).call(iterable); - - this.buffer = Buffer.alloc(0); - this.position = 0; - } - - async waitForChunk() { - const result = await this.iterator.next(); - if (result.done) { - throw new Error('unexpected end of data'); - } - - if (this.position === this.buffer.length) { - this.buffer = result.value; - } else { - this.buffer = Buffer.concat([this.buffer.slice(this.position), result.value]); - } - this.position = 0; - } -} - class Parser { - declare debug: Debug; - declare colMetadata: ColumnMetadata[]; - declare options: ParserOptions; + debug: Debug; + colMetadata: ColumnMetadata[]; + options: ParserOptions; - declare suspended: boolean; - declare next: (() => void) | undefined; - declare streamBuffer: StreamBuffer; + iterator: AsyncIterator | Iterator; + buffer: Buffer; + position: number; static async *parseTokens(iterable: AsyncIterable | Iterable, debug: Debug, options: ParserOptions, colMetadata: ColumnMetadata[] = []) { - let token: Token | undefined; - const onDoneParsing = (t: Token | undefined) => { token = t; }; - - const streamBuffer = new StreamBuffer(iterable); - - const parser = new Parser(streamBuffer, debug, options); + const parser = new Parser(iterable, debug, options); parser.colMetadata = colMetadata; while (true) { try { - await streamBuffer.waitForChunk(); + await parser.waitForChunk(); } catch (err: unknown) { - if (streamBuffer.position === streamBuffer.buffer.length) { + if (parser.position === parser.buffer.length) { return; } throw err; } - if (parser.suspended) { - // Unsuspend and continue from where ever we left off. - parser.suspended = false; - const next = parser.next!; - - next(); - - // Check if a new token was parsed after unsuspension. - if (!parser.suspended && token) { - if (token instanceof ColMetadataToken) { - parser.colMetadata = token.columns; - } + while (parser.buffer.length >= parser.position + 1) { + const type = parser.buffer.readUInt8(parser.position); + parser.position += 1; + const token = parser.readToken(type); + if (token !== undefined) { yield token; } } + } + } - while (!parser.suspended && parser.position + 1 <= parser.buffer.length) { - const type = parser.buffer.readUInt8(parser.position); + readToken(type: number): Token | undefined | Promise { + switch (type) { + case TYPE.DONE: { + return this.readDoneToken(); + } - parser.position += 1; + case TYPE.DONEPROC: { + return this.readDoneProcToken(); + } - if (type === TYPE.COLMETADATA) { - const token = await colMetadataParser(parser); - parser.colMetadata = token.columns; - yield token; - } else if (type === TYPE.ROW) { - yield rowParser(parser); - } else if (type === TYPE.NBCROW) { - yield nbcRowParser(parser); - } else if (tokenParsers[type]) { - tokenParsers[type](parser, parser.options, onDoneParsing); - - // Check if a new token was parsed after unsuspension. - if (!parser.suspended && token) { - if (token instanceof ColMetadataToken) { - parser.colMetadata = token.columns; - } - yield token; - } - } else { - throw new Error('Unknown type: ' + type); - } + case TYPE.DONEINPROC: { + return this.readDoneInProcToken(); + } + + case TYPE.ERROR: { + return this.readErrorToken(); + } + + case TYPE.INFO: { + return this.readInfoToken(); + } + + case TYPE.ENVCHANGE: { + return this.readEnvChangeToken(); + } + + case TYPE.LOGINACK: { + return this.readLoginAckToken(); + } + + case TYPE.RETURNSTATUS: { + return this.readReturnStatusToken(); + } + + case TYPE.ORDER: { + return this.readOrderToken(); + } + + case TYPE.FEDAUTHINFO: { + return this.readFedAuthInfoToken(); + } + + case TYPE.SSPI: { + return this.readSSPIToken(); + } + + case TYPE.COLMETADATA: { + return this.readColMetadataToken(); + } + + case TYPE.RETURNVALUE: { + return this.readReturnValueToken(); + } + + case TYPE.ROW: { + return this.readRowToken(); + } + + case TYPE.NBCROW: { + return this.readNbcRowToken(); + } + + case TYPE.FEATUREEXTACK: { + return this.readFeatureExtAckToken(); + } + + default: { + throw new Error('Unknown type: ' + type); } } } - constructor(streamBuffer: StreamBuffer, debug: Debug, options: ParserOptions) { - this.debug = debug; - this.colMetadata = []; - this.options = options; + readFeatureExtAckToken(): FeatureExtAckToken | Promise { + let result; - this.streamBuffer = streamBuffer; - this.suspended = false; - this.next = undefined; - } + try { + result = featureExtAckParser(this.buffer, this.position, this.options); + } catch (err: any) { + if (err instanceof NotEnoughDataError) { + return this.waitForChunk().then(() => { + return this.readFeatureExtAckToken(); + }); + } + + throw err; + } - get buffer() { - return this.streamBuffer.buffer; + this.position = result.offset; + return result.value; } - get position() { - return this.streamBuffer.position; + async readNbcRowToken(): Promise { + return await nbcRowParser(this); } - set position(value) { - this.streamBuffer.position = value; + async readReturnValueToken(): Promise { + return await returnValueParser(this); } - suspend(next: () => void) { - this.suspended = true; - this.next = next; + async readColMetadataToken(): Promise { + const token = await colMetadataParser(this); + this.colMetadata = token.columns; + return token; } - awaitData(length: number, callback: () => void) { - if (this.position + length <= this.buffer.length) { - callback(); - } else { - this.suspend(() => { - this.awaitData(length, callback); - }); + readSSPIToken(): SSPIToken | Promise { + let result; + + try { + result = sspiParser(this.buffer, this.position, this.options); + } catch (err: any) { + if (err instanceof NotEnoughDataError) { + return this.waitForChunk().then(() => { + return this.readSSPIToken(); + }); + } + + throw err; } - } - readInt8(callback: (data: number) => void) { - this.awaitData(1, () => { - const data = this.buffer.readInt8(this.position); - this.position += 1; - callback(data); - }); + this.position = result.offset; + return result.value; } - readUInt8(callback: (data: number) => void) { - this.awaitData(1, () => { - const data = this.buffer.readUInt8(this.position); - this.position += 1; - callback(data); - }); - } + readFedAuthInfoToken(): FedAuthInfoToken | Promise { + let result; - readInt16LE(callback: (data: number) => void) { - this.awaitData(2, () => { - const data = this.buffer.readInt16LE(this.position); - this.position += 2; - callback(data); - }); - } + try { + result = fedAuthInfoParser(this.buffer, this.position, this.options); + } catch (err: any) { + if (err instanceof NotEnoughDataError) { + return this.waitForChunk().then(() => { + return this.readFedAuthInfoToken(); + }); + } - readInt16BE(callback: (data: number) => void) { - this.awaitData(2, () => { - const data = this.buffer.readInt16BE(this.position); - this.position += 2; - callback(data); - }); - } + throw err; + } - readUInt16LE(callback: (data: number) => void) { - this.awaitData(2, () => { - const data = this.buffer.readUInt16LE(this.position); - this.position += 2; - callback(data); - }); + this.position = result.offset; + return result.value; } - readUInt16BE(callback: (data: number) => void) { - this.awaitData(2, () => { - const data = this.buffer.readUInt16BE(this.position); - this.position += 2; - callback(data); - }); - } + readOrderToken(): OrderToken | Promise { + let result; - readInt32LE(callback: (data: number) => void) { - this.awaitData(4, () => { - const data = this.buffer.readInt32LE(this.position); - this.position += 4; - callback(data); - }); - } + try { + result = orderParser(this.buffer, this.position, this.options); + } catch (err: any) { + if (err instanceof NotEnoughDataError) { + return this.waitForChunk().then(() => { + return this.readOrderToken(); + }); + } - readInt32BE(callback: (data: number) => void) { - this.awaitData(4, () => { - const data = this.buffer.readInt32BE(this.position); - this.position += 4; - callback(data); - }); - } + throw err; + } - readUInt32LE(callback: (data: number) => void) { - this.awaitData(4, () => { - const data = this.buffer.readUInt32LE(this.position); - this.position += 4; - callback(data); - }); + this.position = result.offset; + return result.value; } - readUInt32BE(callback: (data: number) => void) { - this.awaitData(4, () => { - const data = this.buffer.readUInt32BE(this.position); - this.position += 4; - callback(data); - }); - } + readReturnStatusToken(): ReturnStatusToken | Promise { + let result; - readBigInt64LE(callback: (data: bigint) => void) { - this.awaitData(8, () => { - const data = this.buffer.readBigInt64LE(this.position); - this.position += 8; - callback(data); - }); - } + try { + result = returnStatusParser(this.buffer, this.position, this.options); + } catch (err: any) { + if (err instanceof NotEnoughDataError) { + return this.waitForChunk().then(() => { + return this.readReturnStatusToken(); + }); + } - readInt64LE(callback: (data: number) => void) { - this.awaitData(8, () => { - const data = Math.pow(2, 32) * this.buffer.readInt32LE(this.position + 4) + ((this.buffer[this.position + 4] & 0x80) === 0x80 ? 1 : -1) * this.buffer.readUInt32LE(this.position); - this.position += 8; - callback(data); - }); - } + throw err; + } - readInt64BE(callback: (data: number) => void) { - this.awaitData(8, () => { - const data = Math.pow(2, 32) * this.buffer.readInt32BE(this.position) + ((this.buffer[this.position] & 0x80) === 0x80 ? 1 : -1) * this.buffer.readUInt32BE(this.position + 4); - this.position += 8; - callback(data); - }); + this.position = result.offset; + return result.value; } - readBigUInt64LE(callback: (data: bigint) => void) { - this.awaitData(8, () => { - const data = this.buffer.readBigUInt64LE(this.position); - this.position += 8; - callback(data); - }); - } + readLoginAckToken(): LoginAckToken | Promise { + let result; - readUInt64LE(callback: (data: number) => void) { - this.awaitData(8, () => { - const data = Math.pow(2, 32) * this.buffer.readUInt32LE(this.position + 4) + this.buffer.readUInt32LE(this.position); - this.position += 8; - callback(data); - }); - } + try { + result = loginAckParser(this.buffer, this.position, this.options); + } catch (err: any) { + if (err instanceof NotEnoughDataError) { + return this.waitForChunk().then(() => { + return this.readLoginAckToken(); + }); + } - readUInt64BE(callback: (data: number) => void) { - this.awaitData(8, () => { - const data = Math.pow(2, 32) * this.buffer.readUInt32BE(this.position) + this.buffer.readUInt32BE(this.position + 4); - this.position += 8; - callback(data); - }); - } + throw err; + } - readFloatLE(callback: (data: number) => void) { - this.awaitData(4, () => { - const data = this.buffer.readFloatLE(this.position); - this.position += 4; - callback(data); - }); + this.position = result.offset; + return result.value; } - readFloatBE(callback: (data: number) => void) { - this.awaitData(4, () => { - const data = this.buffer.readFloatBE(this.position); - this.position += 4; - callback(data); - }); - } + readEnvChangeToken(): EnvChangeToken | undefined | Promise { + let result; + + try { + result = envChangeParser(this.buffer, this.position, this.options); + } catch (err: any) { + if (err instanceof NotEnoughDataError) { + return this.waitForChunk().then(() => { + return this.readEnvChangeToken(); + }); + } + + throw err; + } - readDoubleLE(callback: (data: number) => void) { - this.awaitData(8, () => { - const data = this.buffer.readDoubleLE(this.position); - this.position += 8; - callback(data); - }); + this.position = result.offset; + return result.value; } - readDoubleBE(callback: (data: number) => void) { - this.awaitData(8, () => { - const data = this.buffer.readDoubleBE(this.position); - this.position += 8; - callback(data); - }); + readRowToken(): RowToken | Promise { + return rowParser(this); } - readUInt24LE(callback: (data: number) => void) { - this.awaitData(3, () => { - const low = this.buffer.readUInt16LE(this.position); - const high = this.buffer.readUInt8(this.position + 2); + readInfoToken(): InfoMessageToken | Promise { + let result; + + try { + result = infoParser(this.buffer, this.position, this.options); + } catch (err: any) { + if (err instanceof NotEnoughDataError) { + return this.waitForChunk().then(() => { + return this.readInfoToken(); + }); + } - this.position += 3; + throw err; + } - callback(low | (high << 16)); - }); + this.position = result.offset; + return result.value; } - readUInt40LE(callback: (data: number) => void) { - this.awaitData(5, () => { - const low = this.buffer.readUInt32LE(this.position); - const high = this.buffer.readUInt8(this.position + 4); + readErrorToken(): ErrorMessageToken | Promise { + let result; - this.position += 5; + try { + result = errorParser(this.buffer, this.position, this.options); + } catch (err: any) { + if (err instanceof NotEnoughDataError) { + return this.waitForChunk().then(() => { + return this.readErrorToken(); + }); + } + + throw err; + } - callback((0x100000000 * high) + low); - }); + this.position = result.offset; + return result.value; } - readUNumeric64LE(callback: (data: number) => void) { - this.awaitData(8, () => { - const low = this.buffer.readUInt32LE(this.position); - const high = this.buffer.readUInt32LE(this.position + 4); + readDoneInProcToken(): DoneInProcToken | Promise { + let result; - this.position += 8; + try { + result = doneInProcParser(this.buffer, this.position, this.options); + } catch (err: any) { + if (err instanceof NotEnoughDataError) { + return this.waitForChunk().then(() => { + return this.readDoneInProcToken(); + }); + } + + throw err; + } - callback((0x100000000 * high) + low); - }); + this.position = result.offset; + return result.value; } - readUNumeric96LE(callback: (data: number) => void) { - this.awaitData(12, () => { - const dword1 = this.buffer.readUInt32LE(this.position); - const dword2 = this.buffer.readUInt32LE(this.position + 4); - const dword3 = this.buffer.readUInt32LE(this.position + 8); + readDoneProcToken(): DoneProcToken | Promise { + let result; - this.position += 12; + try { + result = doneProcParser(this.buffer, this.position, this.options); + } catch (err: any) { + if (err instanceof NotEnoughDataError) { + return this.waitForChunk().then(() => { + return this.readDoneProcToken(); + }); + } - callback(dword1 + (0x100000000 * dword2) + (0x100000000 * 0x100000000 * dword3)); - }); + throw err; + } + + this.position = result.offset; + return result.value; } - readUNumeric128LE(callback: (data: number) => void) { - this.awaitData(16, () => { - const dword1 = this.buffer.readUInt32LE(this.position); - const dword2 = this.buffer.readUInt32LE(this.position + 4); - const dword3 = this.buffer.readUInt32LE(this.position + 8); - const dword4 = this.buffer.readUInt32LE(this.position + 12); + readDoneToken(): DoneToken | Promise { + let result; + + try { + result = doneParser(this.buffer, this.position, this.options); + } catch (err: any) { + if (err instanceof NotEnoughDataError) { + return this.waitForChunk().then(() => { + return this.readDoneToken(); + }); + } - this.position += 16; + throw err; + } - callback(dword1 + (0x100000000 * dword2) + (0x100000000 * 0x100000000 * dword3) + (0x100000000 * 0x100000000 * 0x100000000 * dword4)); - }); + this.position = result.offset; + return result.value; } - // Variable length data + constructor(iterable: AsyncIterable | Iterable, debug: Debug, options: ParserOptions) { + this.debug = debug; + this.colMetadata = []; + this.options = options; - readBuffer(length: number, callback: (data: Buffer) => void) { - this.awaitData(length, () => { - const data = this.buffer.slice(this.position, this.position + length); - this.position += length; - callback(data); - }); - } + this.iterator = ((iterable as AsyncIterable)[Symbol.asyncIterator] || (iterable as Iterable)[Symbol.iterator]).call(iterable); - // Read a Unicode String (BVARCHAR) - readBVarChar(callback: (data: string) => void) { - this.readUInt8((length) => { - this.readBuffer(length * 2, (data) => { - callback(data.toString('ucs2')); - }); - }); + this.buffer = Buffer.alloc(0); + this.position = 0; } - // Read a Unicode String (USVARCHAR) - readUsVarChar(callback: (data: string) => void) { - this.readUInt16LE((length) => { - this.readBuffer(length * 2, (data) => { - callback(data.toString('ucs2')); - }); - }); - } + async waitForChunk() { + const result = await this.iterator.next(); + if (result.done) { + throw new Error('unexpected end of data'); + } - // Read binary data (BVARBYTE) - readBVarByte(callback: (data: Buffer) => void) { - this.readUInt8((length) => { - this.readBuffer(length, callback); - }); - } + if (this.position === this.buffer.length) { + this.buffer = result.value; + } else { + this.buffer = Buffer.concat([this.buffer.slice(this.position), result.value]); + } - // Read binary data (USVARBYTE) - readUsVarByte(callback: (data: Buffer) => void) { - this.readUInt16LE((length) => { - this.readBuffer(length, callback); - }); + this.position = 0; } } diff --git a/src/token/token.ts b/src/token/token.ts index a7fcc1289..222d2bf02 100644 --- a/src/token/token.ts +++ b/src/token/token.ts @@ -274,6 +274,19 @@ export class ResetConnectionEnvChangeToken extends Token { } } +export type EnvChangeToken = + DatabaseEnvChangeToken | + LanguageEnvChangeToken | + CharsetEnvChangeToken | + PacketSizeEnvChangeToken | + BeginTransactionEnvChangeToken | + CommitTransactionEnvChangeToken | + RollbackTransactionEnvChangeToken | + DatabaseMirroringPartnerEnvChangeToken | + ResetConnectionEnvChangeToken | + RoutingEnvChangeToken | + CollationChangeToken; + export class CollationChangeToken extends Token { declare name: 'ENVCHANGE'; declare handlerName: 'onSqlCollationChange'; diff --git a/src/value-parser.ts b/src/value-parser.ts index a0ce96ce3..a4e0db37f 100644 --- a/src/value-parser.ts +++ b/src/value-parser.ts @@ -5,711 +5,766 @@ import { TYPE } from './data-type'; import iconv from 'iconv-lite'; import { sprintf } from 'sprintf-js'; import { bufferToLowerCaseGuid, bufferToUpperCaseGuid } from './guid-parser'; +import { NotEnoughDataError, Result, readBigInt64LE, readDoubleLE, readFloatLE, readInt16LE, readInt32LE, readUInt16LE, readUInt32LE, readUInt8, readUInt24LE, readUInt40LE, readUNumeric64LE, readUNumeric96LE, readUNumeric128LE } from './token/helpers'; const NULL = (1 << 16) - 1; const MAX = (1 << 16) - 1; const THREE_AND_A_THIRD = 3 + (1 / 3); const MONEY_DIVISOR = 10000; -const PLP_NULL = Buffer.from([0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]); -const UNKNOWN_PLP_LEN = Buffer.from([0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]); +const PLP_NULL = 0xFFFFFFFFFFFFFFFFn; +const UNKNOWN_PLP_LEN = 0xFFFFFFFFFFFFFFFEn; const DEFAULT_ENCODING = 'utf8'; -function readTinyInt(parser: Parser, callback: (value: unknown) => void) { - parser.readUInt8(callback); +function readTinyInt(buf: Buffer, offset: number): Result { + return readUInt8(buf, offset); } -function readSmallInt(parser: Parser, callback: (value: unknown) => void) { - parser.readInt16LE(callback); +function readSmallInt(buf: Buffer, offset: number): Result { + return readInt16LE(buf, offset); } -function readInt(parser: Parser, callback: (value: unknown) => void) { - parser.readInt32LE(callback); +function readInt(buf: Buffer, offset: number): Result { + return readInt32LE(buf, offset); } -function readBigInt(parser: Parser, callback: (value: unknown) => void) { - parser.readBigInt64LE((value) => { - callback(value.toString()); - }); +function readBigInt(buf: Buffer, offset: number): Result { + let value; + ({ offset, value } = readBigInt64LE(buf, offset)); + + return new Result(value.toString(), offset); } -function readReal(parser: Parser, callback: (value: unknown) => void) { - parser.readFloatLE(callback); +function readReal(buf: Buffer, offset: number): Result { + return readFloatLE(buf, offset); } -function readFloat(parser: Parser, callback: (value: unknown) => void) { - parser.readDoubleLE(callback); +function readFloat(buf: Buffer, offset: number): Result { + return readDoubleLE(buf, offset); } -function readSmallMoney(parser: Parser, callback: (value: unknown) => void) { - parser.readInt32LE((value) => { - callback(value / MONEY_DIVISOR); - }); +function readSmallMoney(buf: Buffer, offset: number): Result { + let value; + ({ offset, value } = readInt32LE(buf, offset)); + + return new Result(value / MONEY_DIVISOR, offset); } -function readMoney(parser: Parser, callback: (value: unknown) => void) { - parser.readInt32LE((high) => { - parser.readUInt32LE((low) => { - callback((low + (0x100000000 * high)) / MONEY_DIVISOR); - }); - }); +function readMoney(buf: Buffer, offset: number): Result { + let high; + ({ offset, value: high } = readInt32LE(buf, offset)); + + let low; + ({ offset, value: low } = readUInt32LE(buf, offset)); + + return new Result((low + (0x100000000 * high)) / MONEY_DIVISOR, offset); } -function readBit(parser: Parser, callback: (value: unknown) => void) { - parser.readUInt8((value) => { - callback(!!value); - }); +function readBit(buf: Buffer, offset: number): Result { + let value; + ({ offset, value } = readUInt8(buf, offset)); + + return new Result(!!value, offset); } -function valueParse(parser: Parser, metadata: Metadata, options: ParserOptions, callback: (value: unknown) => void): void { +function readValue(buf: Buffer, offset: number, metadata: Metadata, options: ParserOptions): Result { const type = metadata.type; switch (type.name) { case 'Null': - return callback(null); + return new Result(null, offset); - case 'TinyInt': - return readTinyInt(parser, callback); + case 'TinyInt': { + return readTinyInt(buf, offset); + } - case 'SmallInt': - return readSmallInt(parser, callback); + case 'SmallInt': { + return readSmallInt(buf, offset); + } - case 'Int': - return readInt(parser, callback); + case 'Int': { + return readInt(buf, offset); + } - case 'BigInt': - return readBigInt(parser, callback); - - case 'IntN': - return parser.readUInt8((dataLength) => { - switch (dataLength) { - case 0: - return callback(null); - - case 1: - return readTinyInt(parser, callback); - case 2: - return readSmallInt(parser, callback); - case 4: - return readInt(parser, callback); - case 8: - return readBigInt(parser, callback); - - default: - throw new Error('Unsupported dataLength ' + dataLength + ' for IntN'); - } - }); + case 'BigInt': { + return readBigInt(buf, offset); + } - case 'Real': - return readReal(parser, callback); + case 'IntN': { + let dataLength; + ({ offset, value: dataLength } = readUInt8(buf, offset)); - case 'Float': - return readFloat(parser, callback); + switch (dataLength) { + case 0: + return new Result(null, offset); - case 'FloatN': - return parser.readUInt8((dataLength) => { - switch (dataLength) { - case 0: - return callback(null); + case 1: + return readTinyInt(buf, offset); + case 2: + return readSmallInt(buf, offset); + case 4: + return readInt(buf, offset); + case 8: + return readBigInt(buf, offset); - case 4: - return readReal(parser, callback); - case 8: - return readFloat(parser, callback); + default: + throw new Error('Unsupported dataLength ' + dataLength + ' for IntN'); + } + } - default: - throw new Error('Unsupported dataLength ' + dataLength + ' for FloatN'); - } - }); + case 'Real': { + return readReal(buf, offset); + } - case 'SmallMoney': - return readSmallMoney(parser, callback); + case 'Float': { + return readFloat(buf, offset); + } + + case 'FloatN': { + let dataLength; + ({ offset, value: dataLength } = readUInt8(buf, offset)); + + switch (dataLength) { + case 0: + return new Result(null, offset); + + case 4: + return readReal(buf, offset); + case 8: + return readFloat(buf, offset); + + default: + throw new Error('Unsupported dataLength ' + dataLength + ' for FloatN'); + } + } + + case 'SmallMoney': { + return readSmallMoney(buf, offset); + } case 'Money': - return readMoney(parser, callback); + return readMoney(buf, offset); - case 'MoneyN': - return parser.readUInt8((dataLength) => { - switch (dataLength) { - case 0: - return callback(null); + case 'MoneyN': { + let dataLength; + ({ offset, value: dataLength } = readUInt8(buf, offset)); - case 4: - return readSmallMoney(parser, callback); - case 8: - return readMoney(parser, callback); + switch (dataLength) { + case 0: + return new Result(null, offset); - default: - throw new Error('Unsupported dataLength ' + dataLength + ' for MoneyN'); - } - }); + case 4: + return readSmallMoney(buf, offset); + case 8: + return readMoney(buf, offset); - case 'Bit': - return readBit(parser, callback); + default: + throw new Error('Unsupported dataLength ' + dataLength + ' for MoneyN'); + } + } - case 'BitN': - return parser.readUInt8((dataLength) => { - switch (dataLength) { - case 0: - return callback(null); + case 'Bit': { + return readBit(buf, offset); + } + + case 'BitN': { + let dataLength; + ({ offset, value: dataLength } = readUInt8(buf, offset)); - case 1: - return readBit(parser, callback); + switch (dataLength) { + case 0: + return new Result(null, offset); - default: - throw new Error('Unsupported dataLength ' + dataLength + ' for BitN'); - } - }); + case 1: + return readBit(buf, offset); + + default: + throw new Error('Unsupported dataLength ' + dataLength + ' for BitN'); + } + } case 'VarChar': - case 'Char': + case 'Char': { const codepage = metadata.collation!.codepage!; - if (metadata.dataLength === MAX) { - return readMaxChars(parser, codepage, callback); - } else { - return parser.readUInt16LE((dataLength) => { - if (dataLength === NULL) { - return callback(null); - } - - readChars(parser, dataLength!, codepage, callback); - }); + + let dataLength; + ({ offset, value: dataLength } = readUInt16LE(buf, offset)); + + if (dataLength === NULL) { + return new Result(null, offset); } + return readChars(buf, offset, dataLength, codepage); + } + case 'NVarChar': - case 'NChar': - if (metadata.dataLength === MAX) { - return readMaxNChars(parser, callback); - } else { - return parser.readUInt16LE((dataLength) => { - if (dataLength === NULL) { - return callback(null); - } - - readNChars(parser, dataLength!, callback); - }); + case 'NChar': { + let dataLength; + ({ offset, value: dataLength } = readUInt16LE(buf, offset)); + + if (dataLength === NULL) { + return new Result(null, offset); } + return readNChars(buf, offset, dataLength); + } + case 'VarBinary': - case 'Binary': - if (metadata.dataLength === MAX) { - return readMaxBinary(parser, callback); - } else { - return parser.readUInt16LE((dataLength) => { - if (dataLength === NULL) { - return callback(null); - } - - readBinary(parser, dataLength!, callback); - }); + case 'Binary': { + let dataLength; + ({ offset, value: dataLength } = readUInt16LE(buf, offset)); + + if (dataLength === NULL) { + return new Result(null, offset); } - case 'Text': - return parser.readUInt8((textPointerLength) => { - if (textPointerLength === 0) { - return callback(null); - } - - parser.readBuffer(textPointerLength, (_textPointer) => { - parser.readBuffer(8, (_timestamp) => { - parser.readUInt32LE((dataLength) => { - readChars(parser, dataLength!, metadata.collation!.codepage!, callback); - }); - }); - }); - }); - - case 'NText': - return parser.readUInt8((textPointerLength) => { - if (textPointerLength === 0) { - return callback(null); - } - - parser.readBuffer(textPointerLength, (_textPointer) => { - parser.readBuffer(8, (_timestamp) => { - parser.readUInt32LE((dataLength) => { - readNChars(parser, dataLength!, callback); - }); - }); - }); - }); - - case 'Image': - return parser.readUInt8((textPointerLength) => { - if (textPointerLength === 0) { - return callback(null); - } - - parser.readBuffer(textPointerLength, (_textPointer) => { - parser.readBuffer(8, (_timestamp) => { - parser.readUInt32LE((dataLength) => { - readBinary(parser, dataLength!, callback); - }); - }); - }); - }); - - case 'Xml': - return readMaxNChars(parser, callback); + return readBinary(buf, offset, dataLength); + } - case 'SmallDateTime': - return readSmallDateTime(parser, options.useUTC, callback); + case 'Text': { + let textPointerLength; + ({ offset, value: textPointerLength } = readUInt8(buf, offset)); - case 'DateTime': - return readDateTime(parser, options.useUTC, callback); - - case 'DateTimeN': - return parser.readUInt8((dataLength) => { - switch (dataLength) { - case 0: - return callback(null); - - case 4: - return readSmallDateTime(parser, options.useUTC, callback); - case 8: - return readDateTime(parser, options.useUTC, callback); - - default: - throw new Error('Unsupported dataLength ' + dataLength + ' for DateTimeN'); - } - }); - - case 'Time': - return parser.readUInt8((dataLength) => { - if (dataLength === 0) { - return callback(null); - } else { - return readTime(parser, dataLength!, metadata.scale!, options.useUTC, callback); - } - }); + if (textPointerLength === 0) { + return new Result(null, offset); + } - case 'Date': - return parser.readUInt8((dataLength) => { - if (dataLength === 0) { - return callback(null); - } else { - return readDate(parser, options.useUTC, callback); - } - }); - - case 'DateTime2': - return parser.readUInt8((dataLength) => { - if (dataLength === 0) { - return callback(null); - } else { - return readDateTime2(parser, dataLength!, metadata.scale!, options.useUTC, callback); - } - }); - - case 'DateTimeOffset': - return parser.readUInt8((dataLength) => { - if (dataLength === 0) { - return callback(null); - } else { - return readDateTimeOffset(parser, dataLength!, metadata.scale!, callback); - } - }); + // Textpointer + ({ offset } = readBinary(buf, offset, textPointerLength)); - case 'NumericN': - case 'DecimalN': - return parser.readUInt8((dataLength) => { - if (dataLength === 0) { - return callback(null); - } else { - return readNumeric(parser, dataLength!, metadata.precision!, metadata.scale!, callback); - } - }); + // Timestamp + ({ offset } = readBinary(buf, offset, 8)); - case 'UniqueIdentifier': - return parser.readUInt8((dataLength) => { - switch (dataLength) { - case 0: - return callback(null); + let dataLength; + ({ offset, value: dataLength } = readUInt32LE(buf, offset)); + + return readChars(buf, offset, dataLength, metadata.collation!.codepage!); + } - case 0x10: - return readUniqueIdentifier(parser, options, callback); + case 'NText': { + let textPointerLength; + ({ offset, value: textPointerLength } = readUInt8(buf, offset)); - default: - throw new Error(sprintf('Unsupported guid size %d', dataLength! - 1)); - } - }); + if (textPointerLength === 0) { + return new Result(null, offset); + } - case 'UDT': - return readMaxBinary(parser, callback); + // Textpointer + ({ offset } = readBinary(buf, offset, textPointerLength)); - case 'Variant': - return parser.readUInt32LE((dataLength) => { - if (dataLength === 0) { - return callback(null); - } + // Timestamp + ({ offset } = readBinary(buf, offset, 8)); - readVariant(parser, options, dataLength!, callback); - }); + let dataLength; + ({ offset, value: dataLength } = readUInt32LE(buf, offset)); - default: - throw new Error(sprintf('Unrecognised type %s', type.name)); - } -} + return readNChars(buf, offset, dataLength); + } -function readUniqueIdentifier(parser: Parser, options: ParserOptions, callback: (value: unknown) => void) { - parser.readBuffer(0x10, (data) => { - callback(options.lowerCaseGuids ? bufferToLowerCaseGuid(data) : bufferToUpperCaseGuid(data)); - }); -} + case 'Image': { + let textPointerLength; + ({ offset, value: textPointerLength } = readUInt8(buf, offset)); -function readNumeric(parser: Parser, dataLength: number, _precision: number, scale: number, callback: (value: unknown) => void) { - parser.readUInt8((sign) => { - sign = sign === 1 ? 1 : -1; - - let readValue; - if (dataLength === 5) { - readValue = parser.readUInt32LE; - } else if (dataLength === 9) { - readValue = parser.readUNumeric64LE; - } else if (dataLength === 13) { - readValue = parser.readUNumeric96LE; - } else if (dataLength === 17) { - readValue = parser.readUNumeric128LE; - } else { - throw new Error(sprintf('Unsupported numeric dataLength %d', dataLength)); - } - - readValue.call(parser, (value) => { - callback((value * sign) / Math.pow(10, scale)); - }); - }); -} + if (textPointerLength === 0) { + return new Result(null, offset); + } + + // Textpointer + ({ offset } = readBinary(buf, offset, textPointerLength)); + + // Timestamp + ({ offset } = readBinary(buf, offset, 8)); + + let dataLength; + ({ offset, value: dataLength } = readUInt32LE(buf, offset)); + + return readBinary(buf, offset, dataLength); + } -function readVariant(parser: Parser, options: ParserOptions, dataLength: number, callback: (value: unknown) => void) { - return parser.readUInt8((baseType) => { - const type = TYPE[baseType]; + case 'SmallDateTime': { + return readSmallDateTime(buf, offset, options.useUTC); + } - return parser.readUInt8((propBytes) => { - dataLength = dataLength - propBytes - 2; + case 'DateTime': { + return readDateTime(buf, offset, options.useUTC); + } - switch (type.name) { - case 'UniqueIdentifier': - return readUniqueIdentifier(parser, options, callback); + case 'DateTimeN': { + let dataLength; + ({ offset, value: dataLength } = readUInt8(buf, offset)); - case 'Bit': - return readBit(parser, callback); + switch (dataLength) { + case 0: + return new Result(null, offset); - case 'TinyInt': - return readTinyInt(parser, callback); + case 4: + return readSmallDateTime(buf, offset, options.useUTC); + case 8: + return readDateTime(buf, offset, options.useUTC); - case 'SmallInt': - return readSmallInt(parser, callback); + default: + throw new Error('Unsupported dataLength ' + dataLength + ' for DateTimeN'); + } + } - case 'Int': - return readInt(parser, callback); + case 'Time': { + let dataLength; + ({ offset, value: dataLength } = readUInt8(buf, offset)); - case 'BigInt': - return readBigInt(parser, callback); + if (dataLength === 0) { + return new Result(null, offset); + } - case 'SmallDateTime': - return readSmallDateTime(parser, options.useUTC, callback); + return readTime(buf, offset, dataLength, metadata.scale!, options.useUTC); + } - case 'DateTime': - return readDateTime(parser, options.useUTC, callback); + case 'Date': { + let dataLength; + ({ offset, value: dataLength } = readUInt8(buf, offset)); - case 'Real': - return readReal(parser, callback); + if (dataLength === 0) { + return new Result(null, offset); + } - case 'Float': - return readFloat(parser, callback); + return readDate(buf, offset, options.useUTC); + } - case 'SmallMoney': - return readSmallMoney(parser, callback); + case 'DateTime2': { + let dataLength; + ({ offset, value: dataLength } = readUInt8(buf, offset)); - case 'Money': - return readMoney(parser, callback); + if (dataLength === 0) { + return new Result(null, offset); + } - case 'Date': - return readDate(parser, options.useUTC, callback); + return readDateTime2(buf, offset, dataLength, metadata.scale!, options.useUTC); + } + + case 'DateTimeOffset': { + let dataLength; + ({ offset, value: dataLength } = readUInt8(buf, offset)); + + if (dataLength === 0) { + return new Result(null, offset); + } - case 'Time': - return parser.readUInt8((scale) => { - return readTime(parser, dataLength, scale, options.useUTC, callback); - }); + return readDateTimeOffset(buf, offset, dataLength, metadata.scale!); + } - case 'DateTime2': - return parser.readUInt8((scale) => { - return readDateTime2(parser, dataLength, scale, options.useUTC, callback); - }); + case 'NumericN': + case 'DecimalN': { + let dataLength; + ({ offset, value: dataLength } = readUInt8(buf, offset)); - case 'DateTimeOffset': - return parser.readUInt8((scale) => { - return readDateTimeOffset(parser, dataLength, scale, callback); - }); + if (dataLength === 0) { + return new Result(null, offset); + } - case 'VarBinary': - case 'Binary': - return parser.readUInt16LE((_maxLength) => { - readBinary(parser, dataLength, callback); - }); + return readNumeric(buf, offset, dataLength, metadata.precision!, metadata.scale!); + } - case 'NumericN': - case 'DecimalN': - return parser.readUInt8((precision) => { - parser.readUInt8((scale) => { - readNumeric(parser, dataLength, precision, scale, callback); - }); - }); + case 'UniqueIdentifier': { + let dataLength; + ({ offset, value: dataLength } = readUInt8(buf, offset)); - case 'VarChar': - case 'Char': - return parser.readUInt16LE((_maxLength) => { - readCollation(parser, (collation) => { - readChars(parser, dataLength, collation.codepage!, callback); - }); - }); + switch (dataLength) { + case 0: + return new Result(null, offset); - case 'NVarChar': - case 'NChar': - return parser.readUInt16LE((_maxLength) => { - readCollation(parser, (_collation) => { - readNChars(parser, dataLength, callback); - }); - }); + case 0x10: + return readUniqueIdentifier(buf, offset, options); default: - throw new Error('Invalid type!'); + throw new Error(sprintf('Unsupported guid size %d', dataLength! - 1)); } - }); - }); + } + + case 'Variant': { + let dataLength; + ({ offset, value: dataLength } = readUInt32LE(buf, offset)); + + if (dataLength === 0) { + return new Result(null, offset); + } + + return readVariant(buf, offset, options, dataLength); + } + + default: { + throw new Error('Invalid type!'); + } + } +} + +function isPLPStream(metadata: Metadata) { + switch (metadata.type.name) { + case 'VarChar': + case 'NVarChar': + case 'VarBinary': { + return metadata.dataLength === MAX; + } + + case 'Xml': { + return true; + } + + case 'UDT': { + return true; + } + } } -function readBinary(parser: Parser, dataLength: number, callback: (value: unknown) => void) { - return parser.readBuffer(dataLength, callback); +function readUniqueIdentifier(buf: Buffer, offset: number, options: ParserOptions): Result { + let data; + ({ value: data, offset } = readBinary(buf, offset, 0x10)); + + return new Result(options.lowerCaseGuids ? bufferToLowerCaseGuid(data) : bufferToUpperCaseGuid(data), offset); } -function readChars(parser: Parser, dataLength: number, codepage: string, callback: (value: unknown) => void) { - if (codepage == null) { - codepage = DEFAULT_ENCODING; +function readNumeric(buf: Buffer, offset: number, dataLength: number, _precision: number, scale: number): Result { + let sign; + ({ offset, value: sign } = readUInt8(buf, offset)); + + sign = sign === 1 ? 1 : -1; + + let value; + if (dataLength === 5) { + ({ offset, value } = readUInt32LE(buf, offset)); + } else if (dataLength === 9) { + ({ offset, value } = readUNumeric64LE(buf, offset)); + } else if (dataLength === 13) { + ({ offset, value } = readUNumeric96LE(buf, offset)); + } else if (dataLength === 17) { + ({ offset, value } = readUNumeric128LE(buf, offset)); + } else { + throw new Error(sprintf('Unsupported numeric dataLength %d', dataLength)); } - return parser.readBuffer(dataLength, (data) => { - callback(iconv.decode(data, codepage)); - }); + return new Result((value * sign) / Math.pow(10, scale), offset); } -function readNChars(parser: Parser, dataLength: number, callback: (value: unknown) => void) { - parser.readBuffer(dataLength, (data) => { - callback(data.toString('ucs2')); - }); +function readVariant(buf: Buffer, offset: number, options: ParserOptions, dataLength: number): Result { + let baseType; + ({ value: baseType, offset } = readUInt8(buf, offset)); + + const type = TYPE[baseType]; + + let propBytes; + ({ value: propBytes, offset } = readUInt8(buf, offset)); + + dataLength = dataLength - propBytes - 2; + + switch (type.name) { + case 'UniqueIdentifier': + return readUniqueIdentifier(buf, offset, options); + + case 'Bit': + return readBit(buf, offset); + + case 'TinyInt': + return readTinyInt(buf, offset); + + case 'SmallInt': + return readSmallInt(buf, offset); + + case 'Int': + return readInt(buf, offset); + + case 'BigInt': + return readBigInt(buf, offset); + + case 'SmallDateTime': + return readSmallDateTime(buf, offset, options.useUTC); + + case 'DateTime': + return readDateTime(buf, offset, options.useUTC); + + case 'Real': + return readReal(buf, offset); + + case 'Float': + return readFloat(buf, offset); + + case 'SmallMoney': + return readSmallMoney(buf, offset); + + case 'Money': + return readMoney(buf, offset); + + case 'Date': + return readDate(buf, offset, options.useUTC); + + case 'Time': { + let scale; + ({ value: scale, offset } = readUInt8(buf, offset)); + + return readTime(buf, offset, dataLength, scale, options.useUTC); + } + + case 'DateTime2': { + let scale; + ({ value: scale, offset } = readUInt8(buf, offset)); + + return readDateTime2(buf, offset, dataLength, scale, options.useUTC); + } + + case 'DateTimeOffset': { + let scale; + ({ value: scale, offset } = readUInt8(buf, offset)); + + return readDateTimeOffset(buf, offset, dataLength, scale); + } + + case 'VarBinary': + case 'Binary': { + // maxLength (unused?) + ({ offset } = readUInt16LE(buf, offset)); + + return readBinary(buf, offset, dataLength); + } + + case 'NumericN': + case 'DecimalN': { + let precision; + ({ value: precision, offset } = readUInt8(buf, offset)); + + let scale; + ({ value: scale, offset } = readUInt8(buf, offset)); + + return readNumeric(buf, offset, dataLength, precision, scale); + } + + case 'VarChar': + case 'Char': { + // maxLength (unused?) + ({ offset } = readUInt16LE(buf, offset)); + + let collation; + ({ value: collation, offset } = readCollation(buf, offset)); + + return readChars(buf, offset, dataLength, collation.codepage!); + } + + case 'NVarChar': + case 'NChar': { + // maxLength (unused?) + ({ offset } = readUInt16LE(buf, offset)); + + // collation (unsued?) + ({ offset } = readCollation(buf, offset)); + + return readNChars(buf, offset, dataLength); + } + + default: + throw new Error('Invalid type!'); + } } -function readMaxBinary(parser: Parser, callback: (value: unknown) => void) { - return readMax(parser, callback); +function readBinary(buf: Buffer, offset: number, dataLength: number): Result { + if (buf.length < offset + dataLength) { + throw new NotEnoughDataError(offset + dataLength); + } + + return new Result(buf.slice(offset, offset + dataLength), offset + dataLength); } -function readMaxChars(parser: Parser, codepage: string, callback: (value: unknown) => void) { - if (codepage == null) { - codepage = DEFAULT_ENCODING; +function readChars(buf: Buffer, offset: number, dataLength: number, codepage: string): Result { + if (buf.length < offset + dataLength) { + throw new NotEnoughDataError(offset + dataLength); } - readMax(parser, (data) => { - if (data) { - callback(iconv.decode(data, codepage)); - } else { - callback(null); - } - }); + return new Result(iconv.decode(buf.slice(offset, offset + dataLength), codepage ?? DEFAULT_ENCODING), offset + dataLength); } -function readMaxNChars(parser: Parser, callback: (value: string | null) => void) { - readMax(parser, (data) => { - if (data) { - callback(data.toString('ucs2')); - } else { - callback(null); - } - }); +function readNChars(buf: Buffer, offset: number, dataLength: number): Result { + if (buf.length < offset + dataLength) { + throw new NotEnoughDataError(offset + dataLength); + } + + return new Result(buf.toString('ucs2', offset, offset + dataLength), offset + dataLength); } -function readMax(parser: Parser, callback: (value: null | Buffer) => void) { - parser.readBuffer(8, (type) => { - if (type.equals(PLP_NULL)) { - return callback(null); - } else if (type.equals(UNKNOWN_PLP_LEN)) { - return readMaxUnknownLength(parser, callback); - } else { - const low = type.readUInt32LE(0); - const high = type.readUInt32LE(4); - - if (high >= (2 << (53 - 32))) { - console.warn('Read UInt64LE > 53 bits : high=' + high + ', low=' + low); - } +async function readPLPStream(parser: Parser): Promise { + while (parser.buffer.length < parser.position + 8) { + await parser.waitForChunk(); + } - const expectedLength = low + (0x100000000 * high); - return readMaxKnownLength(parser, expectedLength, callback); + const expectedLength = parser.buffer.readBigUInt64LE(parser.position); + parser.position += 8; + + if (expectedLength === PLP_NULL) { + return null; + } + + const chunks: Buffer[] = []; + let currentLength = 0; + + while (true) { + while (parser.buffer.length < parser.position + 4) { + await parser.waitForChunk(); } - }); -} -function readMaxKnownLength(parser: Parser, totalLength: number, callback: (value: null | Buffer) => void) { - const data = Buffer.alloc(totalLength, 0); + const chunkLength = parser.buffer.readUInt32LE(parser.position); + parser.position += 4; - let offset = 0; - function next(done: any) { - parser.readUInt32LE((chunkLength) => { - if (!chunkLength) { - return done(); - } + if (!chunkLength) { + break; + } - parser.readBuffer(chunkLength, (chunk) => { - chunk.copy(data, offset); - offset += chunkLength; + while (parser.buffer.length < parser.position + chunkLength) { + await parser.waitForChunk(); + } - next(done); - }); - }); + chunks.push(parser.buffer.slice(parser.position, parser.position + chunkLength)); + parser.position += chunkLength; + currentLength += chunkLength; } - next(() => { - if (offset !== totalLength) { - throw new Error('Partially Length-prefixed Bytes unmatched lengths : expected ' + totalLength + ', but got ' + offset + ' bytes'); + if (expectedLength !== UNKNOWN_PLP_LEN) { + if (currentLength !== Number(expectedLength)) { + throw new Error('Partially Length-prefixed Bytes unmatched lengths : expected ' + expectedLength + ', but got ' + currentLength + ' bytes'); } + } - callback(data); - }); + return chunks; } -function readMaxUnknownLength(parser: Parser, callback: (value: null | Buffer) => void) { - const chunks: Buffer[] = []; +function readSmallDateTime(buf: Buffer, offset: number, useUTC: boolean): Result { + let days; + ({ offset, value: days } = readUInt16LE(buf, offset)); - let length = 0; - function next(done: any) { - parser.readUInt32LE((chunkLength) => { - if (!chunkLength) { - return done(); - } + let minutes; + ({ offset, value: minutes } = readUInt16LE(buf, offset)); - parser.readBuffer(chunkLength, (chunk) => { - chunks.push(chunk); - length += chunkLength; - - next(done); - }); - }); + let value; + if (useUTC) { + value = new Date(Date.UTC(1900, 0, 1 + days, 0, minutes)); + } else { + value = new Date(1900, 0, 1 + days, 0, minutes); } - next(() => { - callback(Buffer.concat(chunks, length)); - }); + return new Result(value, offset); } -function readSmallDateTime(parser: Parser, useUTC: boolean, callback: (value: Date) => void) { - parser.readUInt16LE((days) => { - parser.readUInt16LE((minutes) => { - let value; - if (useUTC) { - value = new Date(Date.UTC(1900, 0, 1 + days, 0, minutes)); - } else { - value = new Date(1900, 0, 1 + days, 0, minutes); - } - callback(value); - }); - }); -} +function readDateTime(buf: Buffer, offset: number, useUTC: boolean): Result { + let days; + ({ offset, value: days } = readInt32LE(buf, offset)); -function readDateTime(parser: Parser, useUTC: boolean, callback: (value: Date) => void) { - parser.readInt32LE((days) => { - parser.readUInt32LE((threeHundredthsOfSecond) => { - const milliseconds = Math.round(threeHundredthsOfSecond * THREE_AND_A_THIRD); + let threeHundredthsOfSecond; + ({ offset, value: threeHundredthsOfSecond } = readInt32LE(buf, offset)); - let value; - if (useUTC) { - value = new Date(Date.UTC(1900, 0, 1 + days, 0, 0, 0, milliseconds)); - } else { - value = new Date(1900, 0, 1 + days, 0, 0, 0, milliseconds); - } + const milliseconds = Math.round(threeHundredthsOfSecond * THREE_AND_A_THIRD); - callback(value); - }); - }); + let value; + if (useUTC) { + value = new Date(Date.UTC(1900, 0, 1 + days, 0, 0, 0, milliseconds)); + } else { + value = new Date(1900, 0, 1 + days, 0, 0, 0, milliseconds); + } + + return new Result(value, offset); } interface DateWithNanosecondsDelta extends Date { nanosecondsDelta: number; } -function readTime(parser: Parser, dataLength: number, scale: number, useUTC: boolean, callback: (value: DateWithNanosecondsDelta) => void) { - let readValue: any; +function readTime(buf: Buffer, offset: number, dataLength: number, scale: number, useUTC: boolean): Result { + let value; + switch (dataLength) { - case 3: - readValue = parser.readUInt24LE; + case 3: { + ({ value, offset } = readUInt24LE(buf, offset)); break; - case 4: - readValue = parser.readUInt32LE; + } + + case 4: { + ({ value, offset } = readUInt32LE(buf, offset)); break; - case 5: - readValue = parser.readUInt40LE; - } + } - readValue!.call(parser, (value: number) => { - if (scale < 7) { - for (let i = scale; i < 7; i++) { - value *= 10; - } + case 5: { + ({ value, offset } = readUInt40LE(buf, offset)); + break; } - let date; - if (useUTC) { - date = new Date(Date.UTC(1970, 0, 1, 0, 0, 0, value / 10000)) as DateWithNanosecondsDelta; - } else { - date = new Date(1970, 0, 1, 0, 0, 0, value / 10000) as DateWithNanosecondsDelta; + default: { + throw new Error('unreachable'); } - Object.defineProperty(date, 'nanosecondsDelta', { - enumerable: false, - value: (value % 10000) / Math.pow(10, 7) - }); - callback(date); - }); -} + } -function readDate(parser: Parser, useUTC: boolean, callback: (value: Date) => void) { - parser.readUInt24LE((days) => { - if (useUTC) { - callback(new Date(Date.UTC(2000, 0, days - 730118))); - } else { - callback(new Date(2000, 0, days - 730118)); + if (scale < 7) { + for (let i = scale; i < 7; i++) { + value *= 10; } + } + + let date; + if (useUTC) { + date = new Date(Date.UTC(1970, 0, 1, 0, 0, 0, value / 10000)) as DateWithNanosecondsDelta; + } else { + date = new Date(1970, 0, 1, 0, 0, 0, value / 10000) as DateWithNanosecondsDelta; + } + Object.defineProperty(date, 'nanosecondsDelta', { + enumerable: false, + value: (value % 10000) / Math.pow(10, 7) }); + + return new Result(date, offset); } -function readDateTime2(parser: Parser, dataLength: number, scale: number, useUTC: boolean, callback: (value: DateWithNanosecondsDelta) => void) { - readTime(parser, dataLength - 3, scale, useUTC, (time) => { // TODO: 'input' is 'time', but TypeScript cannot find "time.nanosecondsDelta"; - parser.readUInt24LE((days) => { - let date; - if (useUTC) { - date = new Date(Date.UTC(2000, 0, days - 730118, 0, 0, 0, +time)) as DateWithNanosecondsDelta; - } else { - date = new Date(2000, 0, days - 730118, time.getHours(), time.getMinutes(), time.getSeconds(), time.getMilliseconds()) as DateWithNanosecondsDelta; - } - Object.defineProperty(date, 'nanosecondsDelta', { - enumerable: false, - value: time.nanosecondsDelta - }); - callback(date); - }); +function readDate(buf: Buffer, offset: number, useUTC: boolean): Result { + let days; + ({ offset, value: days } = readUInt24LE(buf, offset)); + + if (useUTC) { + return new Result(new Date(Date.UTC(2000, 0, days - 730118)), offset); + } else { + return new Result(new Date(2000, 0, days - 730118), offset); + } +} + +function readDateTime2(buf: Buffer, offset: number, dataLength: number, scale: number, useUTC: boolean): Result { + let time; + ({ offset, value: time } = readTime(buf, offset, dataLength - 3, scale, useUTC)); + + let days; + ({ offset, value: days } = readUInt24LE(buf, offset)); + + let date; + if (useUTC) { + date = new Date(Date.UTC(2000, 0, days - 730118, 0, 0, 0, +time)) as DateWithNanosecondsDelta; + } else { + date = new Date(2000, 0, days - 730118, time.getHours(), time.getMinutes(), time.getSeconds(), time.getMilliseconds()) as DateWithNanosecondsDelta; + } + Object.defineProperty(date, 'nanosecondsDelta', { + enumerable: false, + value: time.nanosecondsDelta }); + + return new Result(date, offset); } -function readDateTimeOffset(parser: Parser, dataLength: number, scale: number, callback: (value: DateWithNanosecondsDelta) => void) { - readTime(parser, dataLength - 5, scale, true, (time) => { - parser.readUInt24LE((days) => { - // offset - parser.readInt16LE(() => { - const date = new Date(Date.UTC(2000, 0, days - 730118, 0, 0, 0, +time)) as DateWithNanosecondsDelta; - Object.defineProperty(date, 'nanosecondsDelta', { - enumerable: false, - value: time.nanosecondsDelta - }); - callback(date); - }); - }); +function readDateTimeOffset(buf: Buffer, offset: number, dataLength: number, scale: number): Result { + let time; + ({ offset, value: time } = readTime(buf, offset, dataLength - 5, scale, true)); + + let days; + ({ offset, value: days } = readUInt24LE(buf, offset)); + + // time offset? + ({ offset } = readUInt16LE(buf, offset)); + + const date = new Date(Date.UTC(2000, 0, days - 730118, 0, 0, 0, +time)) as DateWithNanosecondsDelta; + Object.defineProperty(date, 'nanosecondsDelta', { + enumerable: false, + value: time.nanosecondsDelta }); + return new Result(date, offset); } -export default valueParse; -module.exports = valueParse; +module.exports.readValue = readValue; +module.exports.isPLPStream = isPLPStream; +module.exports.readPLPStream = readPLPStream; + +export { readValue, isPLPStream, readPLPStream }; diff --git a/test/unit/connection-retry-test.js b/test/unit/connection-retry-test.js index 56800db73..6f682fae6 100644 --- a/test/unit/connection-retry-test.js +++ b/test/unit/connection-retry-test.js @@ -22,7 +22,7 @@ function buildLoginAckToken() { 0x00, 0x00, // buildNum ]); - buffer.writeUInt16LE(buffer.length, 1); + buffer.writeUInt16LE(buffer.length - 3, 1); return buffer; } @@ -40,7 +40,7 @@ function buildErrorMessageToken(number, message) { 0x00, 0x00, 0x00, 0x00, // Line Number ]); - buffer.writeUInt16LE(buffer.length, 1); + buffer.writeUInt16LE(buffer.length - 3, 1); buffer.writeUInt32LE(number, 3); buffer.writeUInt16LE(Buffer.byteLength(message, 'ucs2') / 2, 9); diff --git a/test/unit/rerouting-test.js b/test/unit/rerouting-test.js index 22d3cf3cd..2edf3d732 100644 --- a/test/unit/rerouting-test.js +++ b/test/unit/rerouting-test.js @@ -41,7 +41,7 @@ function buildLoginAckToken() { 0x00, 0x00, // buildNum ]); - buffer.writeUInt16LE(buffer.length, 1); + buffer.writeUInt16LE(buffer.length - 3, 1); return buffer; } diff --git a/test/unit/token/env-change-token-parser-test.js b/test/unit/token/env-change-token-parser-test.js index 43919e727..64c098c4a 100644 --- a/test/unit/token/env-change-token-parser-test.js +++ b/test/unit/token/env-change-token-parser-test.js @@ -64,6 +64,8 @@ describe('Env Change Token Parser', () => { const parser = StreamParser.parseTokens([data], {}, {}); const result = await parser.next(); + assert.isTrue(result.done); + assert.isUndefined(result.value); }); });