monetdb 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/mapi.ts CHANGED
@@ -1,10 +1,10 @@
1
- import { Socket, SocketConnectOpts } from "node:net";
2
- import { once, EventEmitter, Abortable } from "events";
3
- import { Buffer, constants } from "buffer";
4
- import { createHash } from "node:crypto";
1
+ import {Socket, SocketConnectOpts} from "node:net";
2
+ import {once, EventEmitter} from "events";
3
+ import {Buffer, constants} from "buffer";
4
+ import {createHash} from "node:crypto";
5
5
  import defaults from "./defaults";
6
- import { URL } from "node:url";
7
- import { FileUploader, FileDownloader } from "./file-transfer";
6
+ import {URL} from "node:url";
7
+ import {FileUploader, FileDownloader} from "./file-transfer";
8
8
 
9
9
  const MAPI_BLOCK_SIZE = 1024 * 8 - 2;
10
10
  const MAPI_HEADER_SIZE = 2;
@@ -31,976 +31,1001 @@ const MAX_REDIRECTS = 10;
31
31
  const MAX_BUFF_SIZE = constants.MAX_LENGTH;
32
32
 
33
33
  enum MAPI_STATE {
34
- INIT = 1,
35
- CONNECTED,
36
- READY,
34
+ INIT = 1,
35
+ CONNECTED,
36
+ READY,
37
37
  }
38
38
 
39
39
  enum MAPI_LANGUAGE {
40
- SQL = "sql",
41
- MAPI = "mapi",
42
- CONTROL = "control", // ? Not implemented
40
+ SQL = "sql",
41
+ MAPI = "mapi",
42
+ CONTROL = "control", // ? Not implemented
43
43
  }
44
44
 
45
45
  interface MapiConfig {
46
- database: string;
47
- username?: string;
48
- password?: string;
49
- language?: MAPI_LANGUAGE;
50
- host?: string;
51
- port?: number;
52
- unixSocket?: string;
53
- timeout?: number;
54
- autoCommit?: boolean;
55
- replySize?: number;
46
+ database: string;
47
+ username?: string;
48
+ password?: string;
49
+ language?: MAPI_LANGUAGE;
50
+ host?: string;
51
+ port?: number;
52
+ unixSocket?: string;
53
+ timeout?: number;
54
+ autoCommit?: boolean;
55
+ replySize?: number;
56
56
  }
57
57
 
58
58
  class HandShakeOption {
59
- level: number;
60
- name: string;
61
- value: any;
62
- fallback?: (v: any) => void;
63
- sent: boolean;
64
- constructor(
65
- level: number,
66
- name: string,
67
- value: any,
68
- fallback: (v: any) => void,
69
- sent = false
70
- ) {
71
- this.level = level;
72
- this.name = name;
73
- this.value = value;
74
- this.fallback = fallback;
75
- this.sent = sent;
76
- }
59
+ level: number;
60
+ name: string;
61
+ value: any;
62
+ fallback?: (v: any) => void;
63
+ sent: boolean;
64
+ constructor(
65
+ level: number,
66
+ name: string,
67
+ value: any,
68
+ fallback: (v: any) => void,
69
+ sent = false
70
+ ) {
71
+ this.level = level;
72
+ this.name = name;
73
+ this.value = value;
74
+ this.fallback = fallback;
75
+ this.sent = sent;
76
+ }
77
77
  }
78
78
 
79
79
  function isMapiUri(uri: string): boolean {
80
- const regx = new RegExp("^mapi:monetdb://*", "i");
81
- return regx.test(uri);
80
+ const regx = new RegExp("^mapi:monetdb://*", "i");
81
+ return regx.test(uri);
82
82
  }
83
83
 
84
84
  function parseMapiUri(uri: string): MapiConfig {
85
- if (isMapiUri(uri)) {
86
- const url = new URL(uri.substring(5));
87
- if (url.hostname) {
88
- const host = url.hostname;
89
- const port = parseInt(url.port);
90
- const username = url.username;
91
- const password = url.password;
92
- const database = url.pathname.split("/")[1];
93
- return {
94
- host,
95
- port,
96
- username,
97
- password,
98
- database,
99
- };
85
+ if (isMapiUri(uri)) {
86
+ const url = new URL(uri.substring(5));
87
+ if (url.hostname) {
88
+ const host = url.hostname;
89
+ const port = parseInt(url.port);
90
+ const username = url.username;
91
+ const password = url.password;
92
+ const database = url.pathname.split("/")[1];
93
+ return {
94
+ host,
95
+ port,
96
+ username,
97
+ password,
98
+ database,
99
+ };
100
+ }
100
101
  }
101
- }
102
- throw new Error(`Invalid MAPI URI ${uri}!`);
102
+ throw new Error(`Invalid MAPI URI ${uri}!`);
103
103
  }
104
104
 
105
105
  // validates and sets defaults on missing properties
106
106
  function createMapiConfig(params: MapiConfig): MapiConfig {
107
- const database =
108
- params && params.database ? params.database : defaults.database;
109
- if (typeof database != "string") {
110
- throw new Error("database name must be string");
111
- }
112
-
113
- const username =
114
- params && params.username ? params.username : defaults.username;
115
- const password =
116
- params && params.password ? params.password : defaults.password;
117
-
118
- let host = params && params.host;
119
- const unixSocket = params && params.unixSocket;
120
- if (!unixSocket && !host) host = defaults.host;
121
- if (typeof host != "string") {
122
- throw new TypeError(`${host} is not valid hostname`);
123
- }
124
- const port =
125
- params && params.port ? Number(params.port) : Number(defaults.port);
126
- if (isNaN(port)) {
127
- throw new TypeError(`${port} is not valid port`);
128
- }
129
-
130
- const timeout = params && params.timeout ? Number(params.timeout) : undefined;
131
- if (timeout && isNaN(timeout)) {
132
- throw new TypeError("timeout must be number");
133
- }
134
- const language =
135
- params && params.language ? params.language : MAPI_LANGUAGE.SQL;
136
- const autoCommit = params.autoCommit || defaults.autoCommit;
137
- const replySize = params.replySize || defaults.replySize;
138
-
139
- return {
140
- database,
141
- username,
142
- password,
143
- language,
144
- host,
145
- port,
146
- timeout,
147
- unixSocket,
148
- autoCommit,
149
- replySize,
150
- };
107
+ const database =
108
+ params && params.database ? params.database : defaults.database;
109
+ if (typeof database != "string") {
110
+ throw new Error("database name must be string");
111
+ }
112
+
113
+ const username =
114
+ params && params.username ? params.username : defaults.username;
115
+ const password =
116
+ params && params.password ? params.password : defaults.password;
117
+
118
+ let host = params && params.host;
119
+ const unixSocket = params && params.unixSocket;
120
+ if (!unixSocket && !host) host = defaults.host;
121
+ if (typeof host != "string") {
122
+ throw new TypeError(`${host} is not valid hostname`);
123
+ }
124
+ const port =
125
+ params && params.port ? Number(params.port) : Number(defaults.port);
126
+ if (isNaN(port)) {
127
+ throw new TypeError(`${port} is not valid port`);
128
+ }
129
+
130
+ const timeout = params && params.timeout ? Number(params.timeout) : undefined;
131
+ if (timeout && isNaN(timeout)) {
132
+ throw new TypeError("timeout must be number");
133
+ }
134
+ const language =
135
+ params && params.language ? params.language : MAPI_LANGUAGE.SQL;
136
+ const autoCommit = params.autoCommit || defaults.autoCommit;
137
+ const replySize = params.replySize || defaults.replySize;
138
+
139
+ return {
140
+ database,
141
+ username,
142
+ password,
143
+ language,
144
+ host,
145
+ port,
146
+ timeout,
147
+ unixSocket,
148
+ autoCommit,
149
+ replySize,
150
+ };
151
151
  }
152
152
 
153
153
  class Column {
154
- table: string;
155
- name: string;
156
- type: string;
157
- length?: number;
158
- index?: number;
159
- constructor(
160
- table: string,
161
- name: string,
162
- type: string,
163
- index?: number,
164
- length?: number
165
- ) {
166
- this.table = table;
167
- this.name = name;
168
- this.type = type;
169
- this.index = index;
170
- this.length = length;
171
- }
154
+ table: string;
155
+ name: string;
156
+ type: string;
157
+ length?: number;
158
+ index?: number;
159
+ constructor(
160
+ table: string,
161
+ name: string,
162
+ type: string,
163
+ index?: number,
164
+ length?: number
165
+ ) {
166
+ this.table = table;
167
+ this.name = name;
168
+ this.type = type;
169
+ this.index = index;
170
+ this.length = length;
171
+ }
172
172
  }
173
173
 
174
174
  type QueryResult = {
175
- id?: number;
176
- type?: string;
177
- queryId?: number;
178
- rowCnt?: number;
179
- affectedRows?: number;
180
- columnCnt?: number;
181
- queryTime?: number; // microseconds
182
- sqlOptimizerTime?: number; // microseconds
183
- malOptimizerTime?: number; // microseconds
184
- columns?: Column[];
185
- headers?: ResponseHeaders;
186
- data?: any[];
175
+ id?: number;
176
+ type?: string;
177
+ queryId?: number;
178
+ rowCnt?: number;
179
+ affectedRows?: number;
180
+ columnCnt?: number;
181
+ queryTime?: number; // microseconds
182
+ sqlOptimizerTime?: number; // microseconds
183
+ malOptimizerTime?: number; // microseconds
184
+ columns?: Column[];
185
+ headers?: ResponseHeaders;
186
+ data?: any[];
187
187
  };
188
188
 
189
189
  class QueryStream extends EventEmitter {
190
- constructor() {
191
- super();
192
- }
190
+ constructor() {
191
+ super();
192
+ }
193
193
 
194
- end(res?: QueryResult) {
195
- this.emit("end", res);
196
- }
194
+ end(res?: QueryResult) {
195
+ this.emit("end", res);
196
+ }
197
197
  }
198
198
 
199
199
  function parseHeaderLine(hdrLine: string): Object {
200
- if (hdrLine.startsWith(MSG_HEADER)) {
201
- const [head, tail] = hdrLine.substring(1).trim().split("#");
202
- let res = {};
203
- const vals = head.trim().split(",\t");
204
- switch (tail.trim()) {
205
- case "table_name":
206
- res = { tableNames: vals };
207
- break;
208
- case "name":
209
- res = { columnNames: vals };
210
- break;
211
- case "type":
212
- res = { columnTypes: vals };
213
- break;
214
- default:
215
- res = {};
216
- }
217
- return res;
218
- }
219
- throw TypeError("Invalid header format!");
200
+ if (hdrLine.startsWith(MSG_HEADER)) {
201
+ const [head, tail] = hdrLine.substring(1).trim().split("#");
202
+ let res = {};
203
+ const vals = head.trim().split(",\t");
204
+ switch (tail.trim()) {
205
+ case "table_name":
206
+ res = {tableNames: vals};
207
+ break;
208
+ case "name":
209
+ res = {columnNames: vals};
210
+ break;
211
+ case "type":
212
+ res = {columnTypes: vals};
213
+ break;
214
+ default:
215
+ res = {};
216
+ }
217
+ return res;
218
+ }
219
+ throw TypeError("Invalid header format!");
220
220
  }
221
221
 
222
222
  function parseTupleLine(line: string, types: string[]): any[] {
223
- if (line.startsWith(MSG_TUPLE) && line.endsWith("]")) {
224
- var resultline = [];
225
- var cCol = 0;
226
- var curtok = "";
227
- var state = "INCRAP";
228
- let endQuotes = 0;
229
- /* mostly adapted from clients/R/MonetDB.R/src/mapisplit.c */
230
- for (var curPos = 2; curPos < line.length - 1; curPos++) {
231
- var chr = line.charAt(curPos);
232
- switch (state) {
233
- case "INCRAP":
234
- if (chr != "\t" && chr != "," && chr != " ") {
235
- if (chr == '"') {
236
- state = "INQUOTES";
237
- } else {
238
- state = "INTOKEN";
239
- curtok += chr;
223
+ if (line.startsWith(MSG_TUPLE) && line.endsWith("]")) {
224
+ var resultline = [];
225
+ var cCol = 0;
226
+ var curtok = "";
227
+ var state = "INCRAP";
228
+ let endQuotes = 0;
229
+ /* mostly adapted from clients/R/MonetDB.R/src/mapisplit.c */
230
+ for (var curPos = 2; curPos < line.length - 1; curPos++) {
231
+ var chr = line.charAt(curPos);
232
+ switch (state) {
233
+ case "INCRAP":
234
+ if (chr != "\t" && chr != "," && chr != " ") {
235
+ if (chr == '"') {
236
+ state = "INQUOTES";
237
+ } else {
238
+ state = "INTOKEN";
239
+ curtok += chr;
240
+ }
241
+ }
242
+ break;
243
+ case "INTOKEN":
244
+ if (chr == "," || curPos == line.length - 2) {
245
+ if (curtok == "NULL" && endQuotes === 0) {
246
+ resultline.push(null);
247
+ } else {
248
+ switch (types[cCol]) {
249
+ case "boolean":
250
+ resultline.push(curtok == "true");
251
+ break;
252
+ case "tinyint":
253
+ case "smallint":
254
+ case "int":
255
+ case "wrd":
256
+ case "bigint":
257
+ resultline.push(parseInt(curtok));
258
+ break;
259
+ case "real":
260
+ case "double":
261
+ case "decimal":
262
+ resultline.push(parseFloat(curtok));
263
+ break;
264
+ case "json":
265
+ try {
266
+ resultline.push(JSON.parse(curtok));
267
+ } catch (e) {
268
+ resultline.push(curtok);
269
+ }
270
+ break;
271
+ default:
272
+ // we need to unescape double quotes
273
+ //valPtr = valPtr.replace(/[^\\]\\"/g, '"');
274
+ resultline.push(curtok);
275
+ break;
276
+ }
277
+ }
278
+ cCol++;
279
+ state = "INCRAP";
280
+ curtok = "";
281
+ endQuotes = 0;
282
+ } else {
283
+ curtok += chr;
284
+ }
285
+ break;
286
+ case "ESCAPED":
287
+ state = "INQUOTES";
288
+ switch (chr) {
289
+ case "t":
290
+ curtok += "\t";
291
+ break;
292
+ case "n":
293
+ curtok += "\n";
294
+ break;
295
+ case "r":
296
+ curtok += "\r";
297
+ break;
298
+ default:
299
+ curtok += chr;
300
+ }
301
+ break;
302
+ case "INQUOTES":
303
+ if (chr == '"') {
304
+ state = "INTOKEN";
305
+ endQuotes++;
306
+ break;
307
+ }
308
+ if (chr == "\\") {
309
+ state = "ESCAPED";
310
+ break;
311
+ }
312
+ curtok += chr;
313
+ break;
240
314
  }
241
- }
242
- break;
243
- case "INTOKEN":
244
- if (chr == "," || curPos == line.length - 2) {
245
- if (curtok == "NULL" && endQuotes === 0) {
246
- resultline.push(null);
247
- } else {
248
- switch (types[cCol]) {
249
- case "boolean":
250
- resultline.push(curtok == "true");
251
- break;
252
- case "tinyint":
253
- case "smallint":
254
- case "int":
255
- case "wrd":
256
- case "bigint":
257
- resultline.push(parseInt(curtok));
258
- break;
259
- case "real":
260
- case "double":
261
- case "decimal":
262
- resultline.push(parseFloat(curtok));
263
- break;
264
- case "json":
265
- try {
266
- resultline.push(JSON.parse(curtok));
267
- } catch (e) {
268
- resultline.push(curtok);
269
- }
270
- break;
271
- default:
272
- // we need to unescape double quotes
273
- //valPtr = valPtr.replace(/[^\\]\\"/g, '"');
274
- resultline.push(curtok);
275
- break;
276
- }
277
- }
278
- cCol++;
279
- state = "INCRAP";
280
- curtok = "";
281
- endQuotes = 0;
282
- } else {
283
- curtok += chr;
284
- }
285
- break;
286
- case "ESCAPED":
287
- state = "INQUOTES";
288
- switch (chr) {
289
- case "t":
290
- curtok += "\t";
291
- break;
292
- case "n":
293
- curtok += "\n";
294
- break;
295
- case "r":
296
- curtok += "\r";
297
- break;
298
- default:
299
- curtok += chr;
300
- }
301
- break;
302
- case "INQUOTES":
303
- if (chr == '"') {
304
- state = "INTOKEN";
305
- endQuotes++;
306
- break;
307
- }
308
- if (chr == "\\") {
309
- state = "ESCAPED";
310
- break;
311
- }
312
- curtok += chr;
313
- break;
314
- }
315
- }
316
- return resultline;
317
- }
318
- throw TypeError("Invalid tuple format!");
315
+ }
316
+ return resultline;
317
+ }
318
+ throw TypeError("Invalid tuple format!");
319
319
  }
320
320
 
321
321
  interface ResponseCallbacks {
322
- resolve: (v: QueryResult | QueryStream | Promise<any>) => void;
323
- reject: (err: Error) => void;
322
+ resolve: (v: QueryResult | QueryStream | Promise<any>) => void;
323
+ reject: (err: Error) => void;
324
324
  }
325
325
 
326
326
  interface ResponseHeaders {
327
- tableNames?: string[];
328
- columnNames?: string[];
329
- columnTypes?: string[];
327
+ tableNames?: string[];
328
+ columnNames?: string[];
329
+ columnTypes?: string[];
330
330
  }
331
331
 
332
332
  interface ResponseOpt {
333
- stream?: boolean;
334
- callbacks?: ResponseCallbacks;
335
- fileHandler?: any;
333
+ stream?: boolean;
334
+ callbacks?: ResponseCallbacks;
335
+ fileHandler?: any;
336
336
  }
337
337
 
338
338
  class Response {
339
- buff: Buffer;
340
- offset: number;
341
- parseOffset: number;
342
- stream: boolean;
343
- settled: boolean;
344
- headerEmitted: boolean;
345
- segments: Segment[];
346
- result?: QueryResult;
347
- callbacks: ResponseCallbacks;
348
- queryStream?: QueryStream;
349
- headers?: ResponseHeaders;
350
- fileHandler: any;
351
-
352
- constructor(opt: ResponseOpt = {}) {
353
- this.buff = Buffer.allocUnsafe(MAPI_BLOCK_SIZE).fill(0);
354
- this.offset = 0;
355
- this.parseOffset = 0;
356
- this.segments = [];
357
- this.settled = false;
358
- this.headerEmitted = false;
359
- this.stream = opt.stream;
360
- this.callbacks = opt.callbacks;
361
- this.fileHandler = opt.fileHandler;
362
- if (opt.stream) {
363
- this.queryStream = new QueryStream();
364
- if (opt.callbacks && opt.callbacks.resolve)
365
- opt.callbacks.resolve(this.queryStream);
366
- }
367
- }
368
-
369
- append(data: Buffer): number {
370
- let srcStartIndx = 0;
371
- let srcEndIndx = srcStartIndx + data.length;
372
- const l = this.segments.length;
373
- let segment = (l > 0 && this.segments[l - 1]) || undefined;
374
- let bytesCopied = 0;
375
- let bytesProcessed = 0;
376
- if (!this.complete()) {
377
- // check if out of space
378
- if (this.buff.length - this.offset < data.length) {
379
- const bytes = this.expand(MAPI_BLOCK_SIZE);
380
- console.log(`expanding by ${bytes} bytes!`);
381
- }
382
-
383
- if (segment === undefined || (segment && segment.isFull())) {
384
- const hdr = data.readUInt16LE(0);
385
- const last = (hdr & 1) === 1;
386
- const bytes = hdr >> 1;
387
- srcStartIndx = MAPI_HEADER_SIZE;
388
- srcEndIndx = srcStartIndx + Math.min(bytes, data.length);
389
- bytesCopied = data.copy(
390
- this.buff,
391
- this.offset,
392
- srcStartIndx,
393
- srcEndIndx
394
- );
395
- segment = new Segment(bytes, last, this.offset, bytesCopied);
396
- this.segments.push(segment);
397
- this.offset += bytesCopied;
398
- bytesProcessed = MAPI_HEADER_SIZE + bytesCopied;
399
- } else {
400
- const byteCntToRead = segment.bytes - segment.bytesOffset;
401
- srcEndIndx = srcStartIndx + byteCntToRead;
402
- bytesCopied = data.copy(
403
- this.buff,
404
- this.offset,
405
- srcStartIndx,
406
- srcEndIndx
407
- );
408
- this.offset += bytesCopied;
409
- segment.bytesOffset += bytesCopied;
410
- // console.log(`segment is full ${segment.bytesOffset === segment.bytes}`);
411
- bytesProcessed = bytesCopied;
412
- }
413
- if (this.isQueryResponse()) {
414
- const tuples = [];
415
- // const firstPackage = this.parseOffset === 0;
416
- this.parseOffset += this.parseQueryResponse(
417
- this.toString(this.parseOffset),
418
- tuples
419
- );
420
- if (tuples.length > 0) {
421
- if (this.queryStream) {
422
- // emit header once
423
- if (
424
- this.headerEmitted === false &&
425
- this.result &&
426
- this.result.columns
427
- ) {
428
- this.queryStream.emit("header", this.result.columns);
429
- this.headerEmitted = true;
430
- }
431
- // emit tuples
432
- this.queryStream.emit("data", tuples);
433
- } else {
434
- this.result.data = this.result.data || [];
435
- for (let t of tuples) {
436
- this.result.data.push(t);
437
- }
438
- }
339
+ chunks: Buffer[];
340
+ buff: Buffer;
341
+ offset: number;
342
+ parseOffset: number;
343
+ stream: boolean;
344
+ settled: boolean;
345
+ headerEmitted: boolean;
346
+ result?: QueryResult;
347
+ callbacks: ResponseCallbacks;
348
+ queryStream?: QueryStream;
349
+ headers?: ResponseHeaders;
350
+ fileHandler: any;
351
+ last: boolean;
352
+ leftOver: number;
353
+ private _str: string;
354
+
355
+ constructor(opt: ResponseOpt = {}) {
356
+ this.chunks = [];
357
+ //this.buff = Buffer.allocUnsafe(MAPI_BLOCK_SIZE).fill(0);
358
+ this.offset = 0;
359
+ this.parseOffset = 0;
360
+ this.settled = false;
361
+ this.last = false;
362
+ this.headerEmitted = false;
363
+ this.stream = opt.stream;
364
+ this.callbacks = opt.callbacks;
365
+ this.fileHandler = opt.fileHandler;
366
+ this.leftOver = 0;
367
+ this._str = "";
368
+ if (opt.stream) {
369
+ this.queryStream = new QueryStream();
370
+ if (opt.callbacks && opt.callbacks.resolve)
371
+ opt.callbacks.resolve(this.queryStream);
439
372
  }
440
- }
441
- }
442
- return bytesProcessed;
443
- }
444
-
445
- complete(): boolean {
446
- const l = this.segments.length;
447
- if (l > 0) {
448
- const segment = this.segments[l - 1];
449
- return segment.last && segment.isFull();
450
- }
451
- return false;
452
- }
453
-
454
- private seekOffset(): number {
455
- const len = this.segments.length;
456
- if (len) {
457
- const last = this.segments[len - 1];
458
- if (last.isFull()) return last.offset + last.bytes;
459
- return last.offset;
460
- }
461
- return 0;
462
- }
463
-
464
- private expand(byteCount: number): number {
465
- if (
466
- this.buff.length + byteCount > MAX_BUFF_SIZE &&
467
- this.fileHandler instanceof FileDownloader
468
- ) {
469
- const offset = this.seekOffset();
470
- if (offset) {
471
- this.fileHandler.writeChunk(this.buff.subarray(0, offset));
472
- this.buff = this.buff.subarray(offset);
473
- this.offset -= offset;
474
- }
475
- }
476
- const buff = Buffer.allocUnsafe(this.buff.length + byteCount).fill(0);
477
- const bytesCopied = this.buff.copy(buff);
478
- this.buff = buff;
479
- // should be byteCount
480
- return this.buff.length - bytesCopied;
481
- }
482
-
483
- private firstCharacter(): string {
484
- return this.buff.toString("utf8", 0, 1);
485
- }
486
-
487
- errorMessage(): string {
488
- if (this.firstCharacter() === MSG_ERROR) {
489
- return this.buff.toString("utf8", 1);
490
- }
491
- return "";
492
- }
493
-
494
- isFileTransfer(): boolean {
495
- return this.toString().startsWith(MSG_FILETRANS);
496
- }
497
-
498
- isPrompt(): boolean {
499
- // perhaps use toString
500
- return this.complete() && this.firstCharacter() === "\x00";
501
- }
502
-
503
- isRedirect(): boolean {
504
- return this.firstCharacter() === MSG_REDIRECT;
505
- }
506
-
507
- isQueryResponse(): boolean {
508
- if (this.result && this.result.type) {
509
- return this.result.type.startsWith(MSG_Q);
510
- }
511
- return this.firstCharacter() === MSG_Q;
512
- }
513
-
514
- isMsgMore(): boolean {
515
- // server wants more ?
516
- return this.toString().startsWith(MSG_MORE);
517
- }
518
-
519
- toString(start?: number) {
520
- const res = this.buff.toString("utf8", 0, this.offset);
521
- if (start) return res.substring(start);
522
- return res;
523
- }
524
-
525
- settle(res?: Promise<any>): void {
526
- if (this.settled === false && this.complete()) {
527
- const errMsg = this.errorMessage();
528
- const err = errMsg ? new Error(errMsg) : null;
529
- if (this.queryStream) {
530
- if (err) this.queryStream.emit("error", err);
531
- this.queryStream.end();
532
- } else {
533
- if (this.callbacks) {
534
- if (err) {
535
- this.callbacks.reject(err);
536
- } else {
537
- this.callbacks.resolve(res || this.result);
538
- }
539
- } else if (this.fileHandler && this.isQueryResponse()) {
540
- this.fileHandler.resolve(this.result);
541
- } else if (this.fileHandler && (err || this.fileHandler.err)) {
542
- this.fileHandler.reject(err || this.fileHandler.err);
373
+ }
374
+
375
+ append(data: Buffer): number {
376
+ //let srcStartIndx = 0;
377
+ //let srcEndIndx = srcStartIndx + data.length;
378
+ let bytesProcessed = 0;
379
+
380
+ if (this.leftOver == 0) {
381
+ // new mapi blk, read header
382
+ const hdr = data.readUInt16LE(0);
383
+ this.last = (hdr & 1) === 1;
384
+ this.leftOver = hdr >> 1;
385
+ data = data.subarray(MAPI_HEADER_SIZE);
386
+ bytesProcessed += MAPI_HEADER_SIZE;
387
+ //bytesCopied = data.copy(
388
+ // this.buff,
389
+ // this.offset,
390
+ // srcStartIndx,
391
+ // srcEndIndx
392
+ //);
393
+ //segment = new Segment(bytes, last, this.offset, bytesCopied);
394
+ //this.segments.push(segment);
395
+ //this.offset += bytesCopied;
396
+ //bytesProcessed = MAPI_HEADER_SIZE + bytesCopied;
397
+ }
398
+ //else {
399
+ // const byteCntToRead = segment.bytes - segment.bytesOffset;
400
+ // srcEndIndx = srcStartIndx + byteCntToRead;
401
+ // bytesCopied = data.copy(
402
+ // this.buff,
403
+ // this.offset,
404
+ // srcStartIndx,
405
+ // srcEndIndx
406
+ // );
407
+ // this.offset += bytesCopied;
408
+ // segment.bytesOffset += bytesCopied;
409
+ // // console.log(`segment is full ${segment.bytesOffset === segment.bytes}`);
410
+ // bytesProcessed = bytesCopied;
411
+ //}
412
+
413
+ const bytesToRead = Math.min(data.length, this.leftOver);
414
+ //console.log(`bytesToRead ${bytesToRead}`);
415
+ if (bytesToRead > 0) {
416
+ const chunk = data.subarray(0, bytesToRead);
417
+ this.chunks.push(chunk);
418
+ this.leftOver -= bytesToRead;
419
+ bytesProcessed += bytesToRead;
420
+ }
421
+
422
+
423
+ //if (this.isQueryResponse()) {
424
+ // const tuples = [];
425
+ // // const firstPackage = this.parseOffset === 0;
426
+ // this.parseOffset += this.parseQueryResponse(
427
+ // this.toString(this.parseOffset),
428
+ // tuples
429
+ // );
430
+ // console.log("tuples");
431
+ // console.log(tuples);
432
+ // if (tuples.length > 0) {
433
+ // if (this.queryStream) {
434
+ // // emit header once
435
+ // if (
436
+ // this.headerEmitted === false &&
437
+ // this.result &&
438
+ // this.result.columns
439
+ // ) {
440
+ // this.queryStream.emit("header", this.result.columns);
441
+ // this.headerEmitted = true;
442
+ // }
443
+ // // emit tuples
444
+ // this.queryStream.emit("data", tuples);
445
+ // } else {
446
+ // this.result.data = this.result.data || [];
447
+ // for (let t of tuples) {
448
+ // this.result.data.push(t);
449
+ // }
450
+ // }
451
+ // }
452
+ //}
453
+ //console.log(`bytesProcessed ${bytesToRead}`);
454
+ return bytesProcessed;
455
+ }
456
+
457
+ complete(): boolean {
458
+ return this.last && (this.leftOver == 0);
459
+ }
460
+
461
+ errorMessage(): string {
462
+ const msg = this.toString();
463
+ if (msg.startsWith(MSG_ERROR)) {
464
+ return msg;
465
+ }
466
+ return "";
467
+ }
468
+
469
+ isFileTransfer(): boolean {
470
+ return this.toString().startsWith(MSG_FILETRANS);
471
+ }
472
+
473
+ isPrompt(): boolean {
474
+ return this.complete() && this.toString() == MSG_PROMPT;
475
+ }
476
+
477
+ isRedirect(): boolean {
478
+ return this.toString().startsWith(MSG_REDIRECT);
479
+ }
480
+
481
+ isQueryResponse(): boolean {
482
+ if (this.result && this.result.type) {
483
+ return this.result.type.startsWith(MSG_Q);
484
+ }
485
+ return this.toString().startsWith(MSG_Q);
486
+ }
487
+
488
+ isMsgMore(): boolean {
489
+ // server wants more ?
490
+ return this.toString().startsWith(MSG_MORE);
491
+ }
492
+
493
+ toString(start?: number) {
494
+ if (this.complete() && !this.queryStream && this._str) {
495
+ if (start)
496
+ return this._str.substring(start);
497
+ return this._str;
543
498
  }
544
- }
545
- this.settled = true;
546
- }
547
- }
548
-
549
- parseQueryResponse(data: string, res: any[]): number {
550
- let offset = 0;
551
- let eol = data.indexOf("\n");
552
- let line = eol > 0 ? data.substring(0, eol) : undefined;
553
- while (line) {
554
- switch (line.charAt(0)) {
555
- case MSG_Q:
556
- // first line
557
- this.result = this.result || {};
558
- this.result.type = line.substring(0, 2);
559
- const rest = line.substring(3).trim().split(" ");
560
- if (this.result.type === MSG_QTABLE) {
561
- const [
562
- id,
563
- rowCnt,
564
- columnCnt,
565
- rows,
566
- queryId,
567
- queryTime,
568
- malOptimizerTime,
569
- sqlOptimizerTime,
570
- ] = rest;
571
- this.result.id = parseInt(id);
572
- this.result.rowCnt = parseInt(rowCnt);
573
- this.result.columnCnt = parseInt(columnCnt);
574
- this.result.queryId = parseInt(queryId);
575
- this.result.queryTime = parseInt(queryTime);
576
- this.result.malOptimizerTime = parseInt(malOptimizerTime);
577
- this.result.sqlOptimizerTime = parseInt(sqlOptimizerTime);
578
- } else if (this.result.type === MSG_QUPDATE) {
579
- const [
580
- affectedRowCnt,
581
- autoIncrementId,
582
- queryId,
583
- queryTime,
584
- malOptimizerTime,
585
- sqlOptimizerTime,
586
- ] = rest;
587
- this.result.affectedRows = parseInt(affectedRowCnt);
588
- this.result.queryId = parseInt(queryId);
589
- this.result.queryTime = parseInt(queryTime);
590
- this.result.malOptimizerTime = parseInt(malOptimizerTime);
591
- this.result.sqlOptimizerTime = parseInt(sqlOptimizerTime);
592
- } else if (this.result.type === MSG_QSCHEMA) {
593
- const [queryTime, malOptimizerTime] = rest;
594
- this.result.queryTime = parseInt(queryTime);
595
- this.result.malOptimizerTime = parseInt(malOptimizerTime);
596
- } else if (this.result.type === MSG_QTRANS) {
597
- // skip
598
- } else if (this.result.type === MSG_QPREPARE) {
599
- const [id, rowCnt, columnCnt, rows] = rest;
600
- this.result.id = parseInt(id);
601
- this.result.rowCnt = parseInt(rowCnt);
602
- this.result.columnCnt = parseInt(columnCnt);
603
- }
604
- break;
605
- case MSG_HEADER:
606
- const header = parseHeaderLine(line);
607
- if (this.result.headers !== undefined) {
608
- this.result.headers = { ...this.result.headers, ...header };
609
- } else {
610
- this.result.headers = header;
611
- }
612
- // if we have all headers we can compile column info
613
- const haveAllHeaders =
614
- Boolean(this.result.headers.tableNames) &&
615
- Boolean(this.result.headers.columnNames) &&
616
- Boolean(this.result.headers.columnTypes);
617
- if (this.result.columns === undefined && haveAllHeaders) {
618
- const colums: Column[] = [];
619
- for (let i = 0; i < this.result.columnCnt; i++) {
620
- const table = this.result.headers.tableNames[i];
621
- const name = this.result.headers.columnNames[i];
622
- const type = this.result.headers.columnTypes[i];
623
- colums.push({
624
- table,
625
- name,
626
- type,
627
- index: i,
628
- });
499
+ const buff = Buffer.concat(this.chunks);
500
+ this._str = buff.toString("utf8");
501
+ if (start) return this._str.substring(start);
502
+ return this._str;
503
+ }
504
+
505
+ settle(res?: Promise<any>): void {
506
+ if (this.settled === false && this.complete()) {
507
+ const errMsg = this.errorMessage();
508
+ const err = errMsg ? new Error(errMsg) : null;
509
+ if (this.queryStream) {
510
+ if (err) this.queryStream.emit("error", err);
511
+ this.queryStream.end();
512
+ } else {
513
+ if (this.callbacks) {
514
+ if (err) {
515
+ this.callbacks.reject(err);
516
+ } else {
517
+ this.callbacks.resolve(res || this.result);
518
+ }
519
+ } else if (this.fileHandler && this.isQueryResponse()) {
520
+ this.fileHandler.resolve(this.result);
521
+ } else if (this.fileHandler && (err || this.fileHandler.err)) {
522
+ this.fileHandler.reject(err || this.fileHandler.err);
523
+ }
629
524
  }
630
- this.result.columns = colums;
631
- }
632
- break;
633
- case MSG_TUPLE:
634
- const tuple = parseTupleLine(line, this.result.headers.columnTypes);
635
- res.push(tuple);
636
- break;
637
- default:
638
- throw TypeError(`Invalid query response line!\n${line}`);
639
- }
640
- // line is processed advance offset
641
- offset = eol + 1;
642
- // get next line
643
- eol = data.indexOf("\n", offset);
644
- line = eol > 0 ? data.substring(offset, eol) : undefined;
645
- }
646
- return offset;
647
- }
648
- }
525
+ this.settled = true;
526
+ this._str = "";
527
+ this.chunks = [];
528
+ }
529
+ }
649
530
 
650
- class Segment {
651
- offset: number; // where segment starts
652
- bytes: number;
653
- bytesOffset: number; // meaningful bytes e.g. if offset + bytesOffset == bytes, then segment full
654
- last: boolean;
655
- constructor(
656
- bytes: number,
657
- last: boolean,
658
- offset: number,
659
- bytesOffset: number
660
- ) {
661
- this.bytes = bytes;
662
- this.last = last;
663
- this.offset = offset;
664
- this.bytesOffset = bytesOffset;
665
- }
666
-
667
- isFull(): boolean {
668
- return this.bytes === this.bytesOffset;
669
- }
531
+ parseQueryResponse(data: string, res: any[]): number {
532
+ let offset = 0;
533
+ let eol = data.indexOf("\n");
534
+ let line = eol > 0 ? data.substring(0, eol) : undefined;
535
+ while (line) {
536
+ switch (line.charAt(0)) {
537
+ case MSG_Q:
538
+ // first line
539
+ this.result = this.result || {};
540
+ this.result.type = line.substring(0, 2);
541
+ const rest = line.substring(3).trim().split(" ");
542
+ if (this.result.type === MSG_QTABLE) {
543
+ const [
544
+ id,
545
+ rowCnt,
546
+ columnCnt,
547
+ rows,
548
+ queryId,
549
+ queryTime,
550
+ malOptimizerTime,
551
+ sqlOptimizerTime,
552
+ ] = rest;
553
+ this.result.id = parseInt(id);
554
+ this.result.rowCnt = parseInt(rowCnt);
555
+ this.result.columnCnt = parseInt(columnCnt);
556
+ this.result.queryId = parseInt(queryId);
557
+ this.result.queryTime = parseInt(queryTime);
558
+ this.result.malOptimizerTime = parseInt(malOptimizerTime);
559
+ this.result.sqlOptimizerTime = parseInt(sqlOptimizerTime);
560
+ } else if (this.result.type === MSG_QUPDATE) {
561
+ const [
562
+ affectedRowCnt,
563
+ autoIncrementId,
564
+ queryId,
565
+ queryTime,
566
+ malOptimizerTime,
567
+ sqlOptimizerTime,
568
+ ] = rest;
569
+ this.result.affectedRows = parseInt(affectedRowCnt);
570
+ this.result.queryId = parseInt(queryId);
571
+ this.result.queryTime = parseInt(queryTime);
572
+ this.result.malOptimizerTime = parseInt(malOptimizerTime);
573
+ this.result.sqlOptimizerTime = parseInt(sqlOptimizerTime);
574
+ } else if (this.result.type === MSG_QSCHEMA) {
575
+ const [queryTime, malOptimizerTime] = rest;
576
+ this.result.queryTime = parseInt(queryTime);
577
+ this.result.malOptimizerTime = parseInt(malOptimizerTime);
578
+ } else if (this.result.type === MSG_QTRANS) {
579
+ // skip
580
+ } else if (this.result.type === MSG_QPREPARE) {
581
+ const [id, rowCnt, columnCnt, rows] = rest;
582
+ this.result.id = parseInt(id);
583
+ this.result.rowCnt = parseInt(rowCnt);
584
+ this.result.columnCnt = parseInt(columnCnt);
585
+ }
586
+ break;
587
+ case MSG_HEADER:
588
+ const header = parseHeaderLine(line);
589
+ if (this.result.headers !== undefined) {
590
+ this.result.headers = {...this.result.headers, ...header};
591
+ } else {
592
+ this.result.headers = header;
593
+ }
594
+ // if we have all headers we can compile column info
595
+ const haveAllHeaders =
596
+ Boolean(this.result.headers.tableNames) &&
597
+ Boolean(this.result.headers.columnNames) &&
598
+ Boolean(this.result.headers.columnTypes);
599
+ if (this.result.columns === undefined && haveAllHeaders) {
600
+ const colums: Column[] = [];
601
+ for (let i = 0; i < this.result.columnCnt; i++) {
602
+ const table = this.result.headers.tableNames[i];
603
+ const name = this.result.headers.columnNames[i];
604
+ const type = this.result.headers.columnTypes[i];
605
+ colums.push({
606
+ table,
607
+ name,
608
+ type,
609
+ index: i,
610
+ });
611
+ }
612
+ this.result.columns = colums;
613
+ }
614
+ break;
615
+ case MSG_TUPLE:
616
+ const tuple = parseTupleLine(line, this.result.headers.columnTypes);
617
+ res.push(tuple);
618
+ break;
619
+ default:
620
+ throw TypeError(`Invalid query response line!\n${line}`);
621
+ }
622
+ // line is processed advance offset
623
+ offset = eol + 1;
624
+ // get next line
625
+ eol = data.indexOf("\n", offset);
626
+ line = eol > 0 ? data.substring(offset, eol) : undefined;
627
+ }
628
+ return offset;
629
+ }
630
+
631
+ streamResult() {
632
+ if (this.queryStream && this.isQueryResponse()) {
633
+ const tuples = [];
634
+ this.parseOffset += this.parseQueryResponse(
635
+ this.toString(this.parseOffset),
636
+ tuples
637
+ );
638
+ if (tuples.length > 0) {
639
+ // emit header once
640
+ if (
641
+ this.headerEmitted === false &&
642
+ this.result &&
643
+ this.result.columns
644
+ ) {
645
+ this.queryStream.emit("header", this.result.columns);
646
+ this.headerEmitted = true;
647
+ }
648
+ // emit tuples
649
+ this.queryStream.emit("data", tuples);
650
+ }
651
+ }
652
+ }
670
653
  }
671
654
 
672
655
  class MapiConnection extends EventEmitter {
673
- state: MAPI_STATE;
674
- socket: Socket;
675
- database: string;
676
- timeout: number;
677
- username: string;
678
- password: string;
679
- host?: string;
680
- unixSocket?: string;
681
- port: number;
682
- language: MAPI_LANGUAGE;
683
- handShakeOptions?: HandShakeOption[];
684
- redirects: number;
685
- queue: Response[];
686
-
687
- constructor(config: MapiConfig) {
688
- super();
689
- this.state = MAPI_STATE.INIT;
690
- this.socket = this.createSocket(config.timeout);
691
- // this.socket = new Socket();
692
- // if (config.timeout) this.socket.setTimeout(config.timeout);
693
- // this.socket.addListener("data", this.recv.bind(this));
694
- // this.socket.addListener("error", this.handleSocketError.bind(this));
695
- // this.socket.addListener("timeout", this.handleTimeout.bind(this));
696
- // this.socket.addListener("close", () => {
697
- // console.log("socket close event");
698
- // this.emit("end");
699
- // });
700
- this.redirects = 0;
701
- this.queue = [];
702
- this.database = config.database;
703
- this.language = config.language || MAPI_LANGUAGE.SQL;
704
- this.unixSocket = config.unixSocket;
705
- this.host = config.host;
706
- this.port = config.port;
707
- this.username = config.username;
708
- this.password = config.password;
709
- this.timeout = config.timeout;
710
- }
711
-
712
- private createSocket = (timeout?: number): Socket => {
713
- const socket = new Socket();
714
- if (timeout) socket.setTimeout(timeout);
715
- socket.addListener("data", this.recv.bind(this));
716
- socket.addListener("error", this.handleSocketError.bind(this));
717
- socket.addListener("timeout", this.handleTimeout.bind(this));
718
- socket.addListener("close", () => {
719
- console.log("socket close event");
720
- this.emit("end");
721
- });
722
- return socket;
723
- };
724
-
725
- connect(handShakeOptions: HandShakeOption[] = []): Promise<any[]> {
726
- this.handShakeOptions = handShakeOptions;
727
- // TODO unix socket
728
- const opt: SocketConnectOpts = {
729
- port: this.port,
730
- host: this.host,
731
- noDelay: true,
732
- };
733
- const socket =
734
- this.socket && !this.socket.destroyed
735
- ? this.socket
736
- : this.createSocket(this.timeout);
737
- socket.connect(opt, () => {
738
- this.state = MAPI_STATE.CONNECTED;
739
- this.socket.setKeepAlive(true);
740
- });
741
- this.socket = socket;
742
-
743
- return once(this, "ready");
744
- }
745
-
746
- ready(): boolean {
747
- return this.state === MAPI_STATE.READY;
748
- }
749
-
750
- disconnect(): Promise<boolean> {
751
- return new Promise((resolve, reject) => {
752
- this.socket.end(() => {
753
- this.redirects = 0;
656
+ state: MAPI_STATE;
657
+ socket: Socket;
658
+ database: string;
659
+ timeout: number;
660
+ username: string;
661
+ password: string;
662
+ host?: string;
663
+ unixSocket?: string;
664
+ port: number;
665
+ language: MAPI_LANGUAGE;
666
+ handShakeOptions?: HandShakeOption[];
667
+ redirects: number;
668
+ queue: Response[];
669
+
670
+ constructor(config: MapiConfig) {
671
+ super();
754
672
  this.state = MAPI_STATE.INIT;
755
- this.socket.destroy();
756
- resolve(this.state === MAPI_STATE.INIT);
757
- });
758
- });
759
- }
760
-
761
- private login(challenge: string): void {
762
- const challengeParts = challenge.split(":");
763
- const [salt, identity, protocol, hashes, endian, algo, opt_level] =
764
- challengeParts;
765
- let password: string;
766
- try {
767
- password = createHash(algo).update(this.password).digest("hex");
768
- } catch (err) {
769
- console.error(err);
770
- this.emit("error", new TypeError(`Algorithm ${algo} not supported`));
771
- return;
772
- }
773
- let pwhash = null;
774
- // try hash algorithms in the order provided by the server
775
- for (const algo of hashes.split(",")) {
776
- try {
777
- const hash = createHash(algo);
778
- pwhash = `{${algo}}` + hash.update(password + salt).digest("hex");
779
- break;
780
- } catch {}
781
- }
782
- if (pwhash) {
783
- let counterResponse = `LIT:${this.username}:${pwhash}:${this.language}:${this.database}:`;
784
- if (opt_level && opt_level.startsWith("sql=")) {
785
- let level = 0;
786
- counterResponse += "FILETRANS:";
673
+ this.redirects = 0;
674
+ this.queue = [];
675
+ this.database = config.database;
676
+ this.language = config.language || MAPI_LANGUAGE.SQL;
677
+ this.unixSocket = config.unixSocket;
678
+ this.host = config.host;
679
+ this.port = config.port;
680
+ this.username = config.username;
681
+ this.password = config.password;
682
+ this.timeout = config.timeout;
683
+ }
684
+
685
+ private createSocket = (timeout?: number): Socket => {
686
+ const socket = new Socket();
687
+ if (timeout) socket.setTimeout(timeout);
688
+ socket.addListener("data", this.recv.bind(this));
689
+ socket.addListener("error", this.handleSocketError.bind(this));
690
+ socket.addListener("timeout", this.handleTimeout.bind(this));
691
+ socket.once("end", () => {
692
+ console.log("Server has ended the connection");
693
+ });
694
+ return socket;
695
+ };
696
+
697
+ connect(handShakeOptions: HandShakeOption[] = []): Promise<any[]> {
698
+ this.handShakeOptions = handShakeOptions;
699
+ // TODO unix socket
700
+ const opt: SocketConnectOpts = {
701
+ port: this.port,
702
+ host: this.host,
703
+ noDelay: true,
704
+ };
705
+ const socket =
706
+ this.socket && !this.socket.destroyed
707
+ ? this.socket
708
+ : this.createSocket(this.timeout);
709
+ socket.connect(opt, () => {
710
+ this.state = MAPI_STATE.CONNECTED;
711
+ this.socket.setKeepAlive(true);
712
+ });
713
+ this.socket = socket;
714
+
715
+ return once(this, "ready");
716
+ }
717
+
718
+ ready(): boolean {
719
+ return this.socket && this.socket.writable && this.state === MAPI_STATE.READY;
720
+ }
721
+
722
+ disconnect(): Promise<true> {
723
+ return new Promise((resolve, reject) => {
724
+ if (!this.socket || this.socket.destroyed) {
725
+ this.socket = null;
726
+ this.state = MAPI_STATE.INIT;
727
+ return resolve(true);
728
+ }
729
+
730
+ const onClose = () => {
731
+ cleanup();
732
+ this.socket = null;
733
+ this.state = MAPI_STATE.INIT;
734
+ resolve(true);
735
+ };
736
+
737
+ const onError = (err: Error) => {
738
+ console.error(err);
739
+ cleanup();
740
+ this.socket?.destroy(); // force cleanup
741
+ this.socket = null;
742
+ this.state = MAPI_STATE.INIT;
743
+ reject(err);
744
+ };
745
+
746
+ const cleanup = () => {
747
+ this.socket?.removeListener("close", onClose);
748
+ this.socket?.removeListener("error", onError);
749
+ };
750
+
751
+ this.socket.once("close", onClose);
752
+ this.socket.once("error", onError);
753
+
754
+ // Initiate graceful shutdown
755
+ this.socket.end();
756
+ });
757
+ }
758
+
759
+ destroy() {
760
+ if (this.socket && !this.socket.destroyed)
761
+ this.socket.destroy();
762
+ this.socket = null;
763
+ }
764
+
765
+ private login(challenge: string): void {
766
+ const challengeParts = challenge.split(":");
767
+ const [salt, identity, protocol, hashes, endian, algo, opt_level] =
768
+ challengeParts;
769
+ let password: string;
787
770
  try {
788
- level = Number(opt_level.substring(4));
771
+ password = createHash(algo).update(this.password).digest("hex");
789
772
  } catch (err) {
790
- this.emit(
791
- "error",
792
- new TypeError("Invalid handshake options level in server challenge")
793
- );
794
- return;
773
+ console.error(err);
774
+ this.emit("error", new TypeError(`Algorithm ${algo} not supported`));
775
+ return;
776
+ }
777
+ let pwhash = null;
778
+ // try hash algorithms in the order provided by the server
779
+ for (const algo of hashes.split(",")) {
780
+ try {
781
+ const hash = createHash(algo);
782
+ pwhash = `{${algo}}` + hash.update(password + salt).digest("hex");
783
+ break;
784
+ } catch {}
795
785
  }
796
- // process handshake options
797
- const options = [];
798
- for (const opt of this.handShakeOptions) {
799
- if (opt.level < level) {
800
- options.push(`${opt.name}=${Number(opt.value)}`);
801
- opt.sent = true;
802
- }
786
+ if (pwhash) {
787
+ let counterResponse = `LIT:${this.username}:${pwhash}:${this.language}:${this.database}:`;
788
+ if (opt_level && opt_level.startsWith("sql=")) {
789
+ let level = 0;
790
+ counterResponse += "FILETRANS:";
791
+ try {
792
+ level = Number(opt_level.substring(4));
793
+ } catch (err) {
794
+ this.emit(
795
+ "error",
796
+ new TypeError("Invalid handshake options level in server challenge")
797
+ );
798
+ return;
799
+ }
800
+ // process handshake options
801
+ const options = [];
802
+ for (const opt of this.handShakeOptions) {
803
+ if (opt.level < level) {
804
+ options.push(`${opt.name}=${Number(opt.value)}`);
805
+ opt.sent = true;
806
+ }
807
+ }
808
+ if (options) counterResponse += options.join(",") + ":";
809
+ }
810
+ this.send(Buffer.from(counterResponse))
811
+ .then(() => this.queue.push(new Response()))
812
+ .catch((err) => this.emit("error", err));
813
+ } else {
814
+ this.emit(
815
+ "error",
816
+ new TypeError(`None of the hashes ${hashes} are supported`)
817
+ );
803
818
  }
804
- if (options) counterResponse += options.join(",") + ":";
805
- }
806
- this.send(Buffer.from(counterResponse))
807
- .then(() => this.queue.push(new Response()))
808
- .catch((err) => this.emit("error", err));
809
- } else {
810
- this.emit(
811
- "error",
812
- new TypeError(`None of the hashes ${hashes} are supported`)
813
- );
814
- }
815
- }
816
-
817
- /**
818
- * Raise exception on server by sending bad packet
819
- */
820
- requestAbort(): Promise<void> {
821
- return new Promise((resolve, reject) => {
822
- const header = Buffer.allocUnsafe(2).fill(0);
823
- // larger than allowed and not final message
824
- header.writeUint16LE(((2 * MAPI_BLOCK_SIZE) << 1) | 0, 0);
825
- // invalid utf8 and too small
826
- const badBody = Buffer.concat([
827
- Buffer.from("ERROR"),
828
- Buffer.from([0x80]),
829
- ]);
830
- const outBuff = Buffer.concat([header, badBody]);
831
- this.socket.write(outBuff, async (err?: Error) => {
832
- if (err) reject(err);
833
- resolve();
834
- });
835
- });
836
- }
837
-
838
- send(buff: Buffer): Promise<void> {
839
- return new Promise((resolve, reject) => {
840
- let last = 0;
841
- let offset = 0;
842
- while (last === 0) {
843
- const seg = buff.subarray(offset, offset + MAPI_BLOCK_SIZE);
844
- last = seg.length < MAPI_BLOCK_SIZE ? 1 : 0;
845
- const header = Buffer.allocUnsafe(2).fill(0);
846
- header.writeUint16LE((seg.length << 1) | last, 0);
847
- const outBuff = Buffer.concat([header, seg]);
848
- this.socket.write(outBuff, (err?: Error) => {
849
- if (err) reject(err);
850
- if (last) resolve();
819
+ }
820
+
821
+ /**
822
+ * Raise exception on server by sending bad packet
823
+ */
824
+ requestAbort(): Promise<void> {
825
+ return new Promise((resolve, reject) => {
826
+ const header = Buffer.allocUnsafe(2).fill(0);
827
+ // larger than allowed and not final message
828
+ header.writeUint16LE(((2 * MAPI_BLOCK_SIZE) << 1) | 0, 0);
829
+ // invalid utf8 and too small
830
+ const badBody = Buffer.concat([
831
+ Buffer.from("ERROR"),
832
+ Buffer.from([0x80]),
833
+ ]);
834
+ const outBuff = Buffer.concat([header, badBody]);
835
+ this.socket.write(outBuff, async (err?: Error) => {
836
+ if (err) reject(err);
837
+ resolve();
838
+ });
851
839
  });
852
- offset += seg.length;
853
- }
854
- });
855
- }
856
-
857
- private handleTimeout() {
858
- this.emit("error", new Error("Timeout"));
859
- }
860
-
861
- private handleSocketError(err: Error) {
862
- console.error(err);
863
- }
864
-
865
- async request(
866
- sql: string,
867
- stream: boolean = false
868
- ): Promise<QueryResult | QueryStream> {
869
- if (this.ready() === false) throw new Error("Not Connected");
870
- await this.send(Buffer.from(sql));
871
- return new Promise((resolve, reject) => {
872
- const resp = new Response({
873
- stream,
874
- callbacks: { resolve, reject },
875
- });
876
- this.queue.push(resp);
877
- });
878
- }
879
-
880
- async requestFileTransfer(buff: Buffer, fileHandler: any): Promise<void> {
881
- await this.send(buff);
882
- const resp = new Response({ fileHandler });
883
- this.queue.push(resp);
884
- }
885
-
886
- async requestFileTransferError(err: string, fileHandler: any): Promise<void> {
887
- await this.send(Buffer.from(err));
888
- const resp = new Response({ fileHandler });
889
- this.queue.push(resp);
890
- }
891
-
892
- private recv(data: Buffer): void {
893
- let bytesLeftOver: number;
894
- let resp: Response;
895
- // process queue left to right, find 1st uncomplete response
896
- // remove responses that are completed
897
- while (this.queue.length) {
898
- const next = this.queue[0];
899
- if (next.complete() || next.settled) {
900
- this.queue.shift();
901
- } else {
902
- resp = next;
903
- break;
904
- }
905
- }
906
- if (resp === undefined && this.queue.length === 0) {
907
- // challenge message
908
- // or direct call to send has being made
909
- // e.g. request api appends Response to the queue
910
- resp = new Response();
911
- this.queue.push(resp);
912
- }
913
-
914
- const offset = resp.append(data);
915
-
916
- if (resp.complete()) this.handleResponse(resp);
917
- bytesLeftOver = data.length - offset;
918
- if (bytesLeftOver) {
919
- const msg = `some ${bytesLeftOver} bytes left over!`;
920
- console.warn(msg);
921
- this.recv(data.subarray(offset));
922
- }
923
- }
924
-
925
- private handleResponse(resp: Response): void {
926
- const err = resp.errorMessage();
927
- if (this.state == MAPI_STATE.CONNECTED) {
928
- if (err) {
929
- this.emit("error", new Error(err));
930
- return;
931
- }
932
- if (resp.isRedirect()) {
933
- this.redirects += 1;
934
- if (this.redirects > MAX_REDIRECTS)
935
- this.emit(
936
- "error",
937
- new Error(`Exceeded max number of redirects ${MAX_REDIRECTS}`)
938
- );
939
- return;
940
- }
941
- if (resp.isPrompt()) {
942
- console.log("login OK");
943
- this.state = MAPI_STATE.READY;
944
- this.emit("ready", this.state);
945
- return;
946
- }
947
- return this.login(resp.toString());
948
- }
949
-
950
- if (resp.isFileTransfer()) {
951
- console.log("file transfer");
952
- let fhandler: any;
953
- const msg = resp.toString(MSG_FILETRANS.length).trim();
954
- let mode: string, offset: string, file: string;
955
- if (msg.startsWith("r ")) {
956
- [mode, offset, file] = msg.split(" ");
957
- fhandler =
958
- resp.fileHandler || new FileUploader(this, file, parseInt(offset));
959
- return resp.settle(fhandler.upload());
960
- } else if (msg.startsWith("rb")) {
961
- [mode, file] = msg.split(" ");
962
- fhandler = resp.fileHandler || new FileUploader(this, file, 0);
963
- return resp.settle(fhandler.upload());
964
- } else if (msg.startsWith("w")) {
965
- [mode, file] = msg.split(" ");
966
- fhandler = resp.fileHandler || new FileDownloader(this, file);
967
- return resp.settle(fhandler.download());
968
- } else {
969
- // no msg end of transfer
970
- const fileHandler = resp.fileHandler;
971
- // we do expect a final response from server
972
- this.queue.push(new Response({ fileHandler }));
973
- return resp.settle(fileHandler.close());
974
- }
975
- }
976
-
977
- if (resp.isMsgMore()) {
978
- // console.log("server wants more");
979
- if (resp.fileHandler instanceof FileUploader)
980
- return resp.settle(resp.fileHandler.upload());
981
- }
982
-
983
- if (
984
- resp.fileHandler instanceof FileDownloader &&
985
- resp.fileHandler.ready()
986
- ) {
987
- // end of download
988
- const fileHandler = resp.fileHandler;
989
- fileHandler.writeChunk(resp.buff);
990
- // we do expect a final response from server
991
- this.queue.push(new Response({ fileHandler }));
992
- return resp.settle(fileHandler.close());
993
- }
994
- resp.settle();
995
- }
840
+ }
841
+
842
+ send(buff: Buffer): Promise<void> {
843
+ return new Promise((resolve, reject) => {
844
+ let last = 0;
845
+ let offset = 0;
846
+ while (last === 0) {
847
+ const seg = buff.subarray(offset, offset + MAPI_BLOCK_SIZE);
848
+ last = seg.length < MAPI_BLOCK_SIZE ? 1 : 0;
849
+ const header = Buffer.allocUnsafe(2).fill(0);
850
+ header.writeUint16LE((seg.length << 1) | last, 0);
851
+ const outBuff = Buffer.concat([header, seg]);
852
+ this.socket.write(outBuff, (err?: Error) => {
853
+ if (err) reject(err);
854
+ if (last) resolve();
855
+ });
856
+ offset += seg.length;
857
+ }
858
+ });
859
+ }
860
+
861
+ private handleTimeout() {
862
+ this.emit("error", new Error("Timeout"));
863
+ }
864
+
865
+ private handleSocketError(err: Error) {
866
+ console.error('Socket error: ', err);
867
+ this.destroy();
868
+ }
869
+
870
+ async request(
871
+ sql: string,
872
+ stream: boolean = false
873
+ ): Promise<QueryResult | QueryStream> {
874
+ if (this.ready() === false) throw new Error("Not Connected");
875
+ await this.send(Buffer.from(sql));
876
+ return new Promise((resolve, reject) => {
877
+ const resp = new Response({
878
+ stream,
879
+ callbacks: {resolve, reject},
880
+ });
881
+ this.queue.push(resp);
882
+ });
883
+ }
884
+
885
+ async requestFileTransfer(buff: Buffer, fileHandler: any): Promise<void> {
886
+ await this.send(buff);
887
+ const resp = new Response({fileHandler});
888
+ this.queue.push(resp);
889
+ }
890
+
891
+ async requestFileTransferError(err: string, fileHandler: any): Promise<void> {
892
+ await this.send(Buffer.from(err));
893
+ const resp = new Response({fileHandler});
894
+ this.queue.push(resp);
895
+ }
896
+
897
+ private recv(data: Buffer): void {
898
+ let bytesLeftOver: number;
899
+ let resp: Response;
900
+ // process queue left to right,
901
+ // find 1st uncomplete response
902
+ // remove complete responses
903
+ while (this.queue.length) {
904
+ const next = this.queue[0];
905
+ if (next.complete() || next.settled) {
906
+ this.queue.shift();
907
+ } else {
908
+ resp = next;
909
+ break;
910
+ }
911
+ }
912
+ if (resp === undefined && this.queue.length === 0) {
913
+ // challenge message
914
+ // or direct call to send has being made
915
+ // request api appends Response to the queue
916
+ resp = new Response();
917
+ this.queue.push(resp);
918
+ }
919
+ let offset = 0;
920
+ let end = data.length;
921
+ do {
922
+ offset += resp.append(data.subarray(offset, end));
923
+ } while ((offset < end) && !resp.complete());
924
+
925
+ if (resp.queryStream)
926
+ resp.streamResult();
927
+
928
+ if (resp.complete())
929
+ this.handleResponse(resp);
930
+
931
+ bytesLeftOver = data.length - offset;
932
+ if (bytesLeftOver) {
933
+ //const msg = `some ${bytesLeftOver} bytes left over!`;
934
+ //console.warn(msg);
935
+ this.recv(data.subarray(offset));
936
+ }
937
+ }
938
+
939
+ private handleResponse(resp: Response): void {
940
+ const err = resp.errorMessage();
941
+ if (this.state == MAPI_STATE.CONNECTED) {
942
+ if (err) {
943
+ this.emit("error", new Error(err));
944
+ return;
945
+ }
946
+ if (resp.isRedirect()) {
947
+ this.redirects += 1;
948
+ if (this.redirects > MAX_REDIRECTS)
949
+ this.emit(
950
+ "error",
951
+ new Error(`Exceeded max number of redirects ${MAX_REDIRECTS}`)
952
+ );
953
+ return;
954
+ }
955
+ if (resp.isPrompt()) {
956
+ //console.log("login OK");
957
+ this.state = MAPI_STATE.READY;
958
+ this.emit("ready", this.state);
959
+ return;
960
+ }
961
+ return this.login(resp.toString());
962
+ }
963
+
964
+ if (resp.isFileTransfer()) {
965
+ //console.log("file transfer");
966
+ let fhandler: any;
967
+ const msg = resp.toString(MSG_FILETRANS.length).trim();
968
+ let mode: string, offset: string, file: string;
969
+ if (msg.startsWith("r ")) {
970
+ [mode, offset, file] = msg.split(" ");
971
+ fhandler =
972
+ resp.fileHandler || new FileUploader(this, file, parseInt(offset));
973
+ return resp.settle(fhandler.upload());
974
+ } else if (msg.startsWith("rb")) {
975
+ [mode, file] = msg.split(" ");
976
+ fhandler = resp.fileHandler || new FileUploader(this, file, 0);
977
+ return resp.settle(fhandler.upload());
978
+ } else if (msg.startsWith("w")) {
979
+ [mode, file] = msg.split(" ");
980
+ fhandler = resp.fileHandler || new FileDownloader(this, file);
981
+ return resp.settle(fhandler.download());
982
+ } else {
983
+ // no msg end of transfer
984
+ const fileHandler = resp.fileHandler;
985
+ // we do expect a final response from server
986
+ this.queue.push(new Response({fileHandler}));
987
+ return resp.settle(fileHandler.close());
988
+ }
989
+ }
990
+
991
+ if (resp.isMsgMore()) {
992
+ // console.log("server wants more");
993
+ if (resp.fileHandler instanceof FileUploader)
994
+ return resp.settle(resp.fileHandler.upload());
995
+ }
996
+
997
+ if (
998
+ resp.fileHandler instanceof FileDownloader &&
999
+ resp.fileHandler.ready()
1000
+ ) {
1001
+ // end of download
1002
+ const fileHandler = resp.fileHandler;
1003
+ const buff = Buffer.concat(resp.chunks);
1004
+ fileHandler.writeChunk(buff);
1005
+ // we do expect a final response from server
1006
+ this.queue.push(new Response({fileHandler}));
1007
+ return resp.settle(fileHandler.close());
1008
+ }
1009
+
1010
+ if (resp.isQueryResponse()) {
1011
+ const data = [];
1012
+ resp.parseQueryResponse(
1013
+ resp.toString(),
1014
+ data
1015
+ );
1016
+ resp.result.data = data;
1017
+ }
1018
+
1019
+ resp.settle();
1020
+ }
996
1021
  }
997
1022
 
998
1023
  export {
999
- MapiConfig,
1000
- MapiConnection,
1001
- parseMapiUri,
1002
- createMapiConfig,
1003
- HandShakeOption,
1004
- QueryResult,
1005
- QueryStream,
1024
+ MapiConfig,
1025
+ MapiConnection,
1026
+ parseMapiUri,
1027
+ createMapiConfig,
1028
+ HandShakeOption,
1029
+ QueryResult,
1030
+ QueryStream,
1006
1031
  };