node-firebird 2.3.4 → 2.4.1

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.
Files changed (109) hide show
  1. package/README.md +574 -20
  2. package/lib/callback.d.ts +20 -0
  3. package/lib/callback.js +6 -16
  4. package/lib/firebird.msg.json +1370 -1370
  5. package/lib/gdscodes.d.ts +1519 -1520
  6. package/lib/gdscodes.js +5 -5
  7. package/lib/ieee754-decimal.d.ts +38 -0
  8. package/lib/ieee754-decimal.js +71 -118
  9. package/lib/index.d.ts +37 -338
  10. package/lib/index.js +97 -86
  11. package/lib/messages.d.ts +6 -0
  12. package/lib/messages.js +135 -162
  13. package/lib/pool.d.ts +23 -0
  14. package/lib/pool.js +34 -47
  15. package/lib/srp.d.ts +51 -0
  16. package/lib/srp.js +62 -72
  17. package/lib/types.d.ts +375 -0
  18. package/lib/types.js +22 -0
  19. package/lib/unix-crypt.d.ts +1 -0
  20. package/lib/unix-crypt.js +126 -152
  21. package/lib/utils.d.ts +16 -0
  22. package/lib/utils.js +24 -48
  23. package/lib/wire/connection.d.ts +100 -0
  24. package/lib/wire/connection.js +1029 -1009
  25. package/lib/wire/const.d.ts +567 -0
  26. package/lib/wire/const.js +620 -599
  27. package/lib/wire/database.d.ts +57 -0
  28. package/lib/wire/database.js +111 -108
  29. package/lib/wire/eventConnection.d.ts +16 -0
  30. package/lib/wire/eventConnection.js +29 -31
  31. package/lib/wire/fbEventManager.d.ts +45 -0
  32. package/lib/wire/fbEventManager.js +38 -57
  33. package/lib/wire/serialize.d.ts +97 -0
  34. package/lib/wire/serialize.js +42 -141
  35. package/lib/wire/service.d.ts +66 -0
  36. package/lib/wire/service.js +374 -401
  37. package/lib/wire/socket.d.ts +59 -0
  38. package/lib/wire/socket.js +79 -40
  39. package/lib/wire/statement.d.ts +25 -0
  40. package/lib/wire/statement.js +13 -9
  41. package/lib/wire/transaction.d.ts +21 -0
  42. package/lib/wire/transaction.js +33 -61
  43. package/lib/wire/xsqlvar.d.ts +175 -0
  44. package/lib/wire/xsqlvar.js +169 -269
  45. package/package.json +12 -4
  46. package/src/callback.ts +55 -0
  47. package/{poc/node_modules/node-firebird/lib/gdscodes.js → src/gdscodes.ts} +3 -3
  48. package/{poc/node_modules/node-firebird/lib/ieee754-decimal.js → src/ieee754-decimal.ts} +7 -14
  49. package/src/index.ts +157 -0
  50. package/{poc/node_modules/node-firebird/lib/messages.js → src/messages.ts} +163 -162
  51. package/src/pool.ts +209 -0
  52. package/{poc/node_modules/node-firebird/lib/srp.js → src/srp.ts} +74 -33
  53. package/src/types.ts +419 -0
  54. package/{poc/node_modules/node-firebird/lib/unix-crypt.js → src/unix-crypt.ts} +6 -12
  55. package/{poc/node_modules/node-firebird/lib/utils.js → src/utils.ts} +11 -20
  56. package/{poc/node_modules/node-firebird/lib/wire/connection.js → src/wire/connection.ts} +611 -129
  57. package/{poc/node_modules/node-firebird/lib/wire/const.js → src/wire/const.ts} +96 -9
  58. package/{poc/node_modules/node-firebird/lib/wire/database.js → src/wire/database.ts} +110 -44
  59. package/{poc/node_modules/node-firebird/lib/wire/eventConnection.js → src/wire/eventConnection.ts} +29 -14
  60. package/{poc/node_modules/node-firebird/lib/wire/fbEventManager.js → src/wire/fbEventManager.ts} +39 -30
  61. package/{poc/node_modules/node-firebird/lib/wire/serialize.js → src/wire/serialize.ts} +78 -59
  62. package/{poc/node_modules/node-firebird/lib/wire/service.js → src/wire/service.ts} +90 -88
  63. package/src/wire/socket.ts +262 -0
  64. package/src/wire/statement.ts +71 -0
  65. package/{poc/node_modules/node-firebird/lib/wire/transaction.js → src/wire/transaction.ts} +25 -19
  66. package/{poc/node_modules/node-firebird/lib/wire/xsqlvar.js → src/wire/xsqlvar.ts} +244 -143
  67. package/.eslintrc.json +0 -12
  68. package/.github/workflows/codeql.yml +0 -76
  69. package/.github/workflows/node.js.yml +0 -95
  70. package/BIGINT_MIGRATION.md +0 -374
  71. package/CI_DEBUGGING_GUIDE.md +0 -148
  72. package/ENCRYPTION_CALLBACK.md +0 -152
  73. package/FIREBIRD_LOG_FEATURE.md +0 -145
  74. package/MINIMAL_CHANGES_SUMMARY.md +0 -136
  75. package/PR_SUMMARY.md +0 -96
  76. package/ROADMAP.md +0 -223
  77. package/SRP_PROTOCOL.md +0 -482
  78. package/poc/README.md +0 -160
  79. package/poc/helpers.js +0 -59
  80. package/poc/node_modules/.package-lock.json +0 -14
  81. package/poc/node_modules/node-firebird/.eslintrc.json +0 -12
  82. package/poc/node_modules/node-firebird/.github/workflows/codeql.yml +0 -76
  83. package/poc/node_modules/node-firebird/.github/workflows/node.js.yml +0 -95
  84. package/poc/node_modules/node-firebird/BIGINT_MIGRATION.md +0 -374
  85. package/poc/node_modules/node-firebird/CI_DEBUGGING_GUIDE.md +0 -148
  86. package/poc/node_modules/node-firebird/ENCRYPTION_CALLBACK.md +0 -152
  87. package/poc/node_modules/node-firebird/FIREBIRD_LOG_FEATURE.md +0 -145
  88. package/poc/node_modules/node-firebird/LICENSE +0 -373
  89. package/poc/node_modules/node-firebird/MINIMAL_CHANGES_SUMMARY.md +0 -136
  90. package/poc/node_modules/node-firebird/PR_SUMMARY.md +0 -96
  91. package/poc/node_modules/node-firebird/README.md +0 -794
  92. package/poc/node_modules/node-firebird/ROADMAP.md +0 -223
  93. package/poc/node_modules/node-firebird/SRP_PROTOCOL.md +0 -482
  94. package/poc/node_modules/node-firebird/lib/callback.js +0 -38
  95. package/poc/node_modules/node-firebird/lib/gdscodes.d.ts +0 -1524
  96. package/poc/node_modules/node-firebird/lib/index.d.ts +0 -316
  97. package/poc/node_modules/node-firebird/lib/index.js +0 -128
  98. package/poc/node_modules/node-firebird/lib/pool.js +0 -108
  99. package/poc/node_modules/node-firebird/lib/wire/socket.js +0 -175
  100. package/poc/node_modules/node-firebird/lib/wire/statement.js +0 -48
  101. package/poc/node_modules/node-firebird/package.json +0 -38
  102. package/poc/node_modules/node-firebird/vitest.config.js +0 -24
  103. package/poc/package-lock.json +0 -21
  104. package/poc/package.json +0 -12
  105. package/poc/reproduce-fixed.js +0 -150
  106. package/poc/reproduce.js +0 -133
  107. package/vitest.config.js +0 -27
  108. /package/{poc/node_modules/node-firebird/lib → src}/firebird.msg +0 -0
  109. /package/{poc/node_modules/node-firebird/lib → src}/firebird.msg.json +0 -0
@@ -1,19 +1,30 @@
1
- const Events = require('events');
2
- const os = require('os');
3
- const path = require('path');
4
-
5
- const {XdrWriter, BlrWriter, XdrReader, BitSet, BlrReader} = require('./serialize');
6
- const {doCallback, doError} = require('../callback');
7
- const srp = require('../srp');
8
- const crypt = require('../unix-crypt');
9
- const Const = require('./const');
10
- const Xsql = require('./xsqlvar');
11
- const ServiceManager = require('./service');
12
- const Database = require('./database');
13
- const Statement = require('./statement');
14
- const Transaction = require('./transaction');
15
- const {lookupMessages, noop, parseDate} = require('../utils');
16
- const Socket = require("./socket");
1
+ import Events from 'events';
2
+ import os from 'os';
3
+ import path from 'path';
4
+
5
+ import { XdrWriter, BlrWriter, XdrReader, BitSet, BlrReader } from './serialize';
6
+ import { doCallback, doError } from '../callback';
7
+ import * as srp from '../srp';
8
+ import * as crypt from '../unix-crypt';
9
+ import Const from './const';
10
+ import * as Xsql from './xsqlvar';
11
+ import ServiceManager from './service';
12
+ import Database from './database';
13
+ import Statement from './statement';
14
+ import Transaction from './transaction';
15
+ import { lookupMessages, noop, parseDate } from '../utils';
16
+ import Socket from './socket';
17
+
18
+ function parseValueIfJson(value: any, options: any) {
19
+ if (options && options.jsonAsObject && typeof value === 'string' && (value.startsWith('{') || value.startsWith('['))) {
20
+ try {
21
+ return JSON.parse(value);
22
+ } catch (e) {
23
+ // Ignore parse error and keep as string
24
+ }
25
+ }
26
+ return value;
27
+ }
17
28
 
18
29
  /***************************************
19
30
  *
@@ -22,7 +33,47 @@ const Socket = require("./socket");
22
33
  ***************************************/
23
34
 
24
35
  class Connection {
25
- constructor(host, port, callback, options, db, svc) {
36
+ static decodeResponse: typeof decodeResponse;
37
+ static fetch_blob_async_transaction: typeof fetch_blob_async_transaction;
38
+ static fetch_blob_async: typeof fetch_blob_async;
39
+ static parseValueIfJson: typeof parseValueIfJson;
40
+ static describe: typeof describe;
41
+
42
+ db: any;
43
+ svc: any;
44
+ options: any;
45
+ accept: any;
46
+ error: any;
47
+ dbhandle: number | undefined;
48
+ svchandle: number | undefined;
49
+ clientKeys: srp.KeyPair | undefined;
50
+ serverKeys: { salt: string; public: bigint; pluginName: string } | undefined;
51
+
52
+ _msg: XdrWriter;
53
+ _blr: BlrWriter;
54
+ _queue: any[];
55
+ _pending: string[];
56
+ _socket: any;
57
+ _xdr: any;
58
+ _isOpened: boolean;
59
+ _isClosed: boolean;
60
+ _isDetach: boolean;
61
+ _isUsed: boolean;
62
+ _pooled: boolean;
63
+ _lowercase_keys: boolean | undefined;
64
+ _detachTimeout: any;
65
+ _detachCallback: any;
66
+ _detachAuto: any;
67
+ _retry_connection_id: any;
68
+ _retry_connection_interval: number;
69
+ _max_cached_query: number;
70
+ _cache_query: Record<string, any> | null;
71
+ _messageFile: string;
72
+ _authStartTime: number | undefined;
73
+ _pendingAccept: any;
74
+ _inlineBlobs: Map<string, Buffer> | undefined;
75
+
76
+ constructor(host: string, port: number, callback: any, options: any, db?: any, svc?: any) {
26
77
  var self = this;
27
78
  this.db = db;
28
79
  this.svc = svc
@@ -40,6 +91,7 @@ class Connection {
40
91
  this._isUsed = false;
41
92
  this._pooled = options.isPool||false;
42
93
  if (options && options.blobChunkSize > 65535) options.blobChunkSize = 65535;
94
+ if (options && options.blobReadChunkSize > 65535) options.blobReadChunkSize = 65535;
43
95
  this.options = options;
44
96
  this._bind_events(host, port, callback);
45
97
  this.error;
@@ -67,55 +119,76 @@ class Connection {
67
119
  }
68
120
 
69
121
 
122
+ // Reject every request that is still awaiting a server response (e.g. a
123
+ // transaction.commit() sent right before the server went away) instead of
124
+ // leaving its callback to hang forever. Requests already in flight cannot
125
+ // be transparently resumed on a reconnect: a fresh attach() hands out new
126
+ // transaction/statement handles, so nothing the server sends back could
127
+ // ever match a callback queued against the old connection.
128
+ _rejectPending(err) {
129
+ var queue = this._queue;
130
+ this._queue = [];
131
+ this._pending = [];
132
+
133
+ for (var i = 0; i < queue.length; i++) {
134
+ doError(err, queue[i]);
135
+ }
136
+ }
137
+
138
+
70
139
  _bind_events(host, port, callback) {
71
-
140
+
72
141
  var self = this;
73
-
142
+
74
143
  self._socket.on('close', function() {
75
-
144
+
76
145
  if (!self._isOpened || self._isDetach) {
77
146
  return;
78
147
  }
79
-
148
+
80
149
  self._isOpened = false;
81
-
150
+ self._isClosed = true;
151
+
152
+ var lostError = self.error || new Error('Connection to Firebird server was lost.');
153
+
82
154
  if (!self.db) {
155
+ self._rejectPending(lostError);
83
156
  if (callback)
84
157
  callback(self.error);
85
158
  return;
86
159
  }
87
-
160
+
161
+ self._rejectPending(lostError);
162
+
88
163
  self._retry_connection_id = setTimeout(function() {
89
164
  self._socket.removeAllListeners();
90
165
  self._socket = null;
91
-
166
+
92
167
  var ctx = new Connection(host, port, function(err) {
93
168
  ctx.connect(self.options, function(err) {
94
-
169
+
95
170
  if (err) {
96
171
  self.db.emit('error', err);
97
172
  return;
98
173
  }
99
-
174
+
100
175
  ctx.attach(self.options, function(err) {
101
-
176
+
102
177
  if (err) {
103
178
  self.db.emit('error', err);
104
179
  return;
105
180
  }
106
-
107
- ctx._queue = ctx._queue.concat(self._queue);
108
- ctx._pending = ctx._pending.concat(self._pending);
181
+
109
182
  self.db.emit('reconnect');
110
-
183
+
111
184
  }, self.db);
112
185
  });
113
-
186
+
114
187
  Object.assign(self, ctx);
115
-
188
+
116
189
  }, self.options, self.db);
117
190
  }, self._retry_connection_interval);
118
-
191
+
119
192
  });
120
193
 
121
194
  self._socket.on('error', function(e) {
@@ -155,6 +228,7 @@ class Connection {
155
228
 
156
229
  while (xdr.pos < xdr.buffer.length) {
157
230
  var cb = self._queue[0], pos = xdr.pos;
231
+ var lazySnapshot = cb ? cb.lazy_count : undefined;
158
232
 
159
233
  decodeResponse(xdr, cb, self, self._lowercase_keys, function (err, obj) {
160
234
 
@@ -171,8 +245,17 @@ class Connection {
171
245
  xdr.buffer.length, pos, self._queue.length);
172
246
  }
173
247
 
174
- if (self.accept && self.accept.protocolMinimumType === Const.ptype_lazy_send && self._queue.length > 0) {
175
- self._queue[0].lazy_count = 2;
248
+ // Restore lazy_count to its value before this (failed)
249
+ // decode attempt. Forcing lazy_count = 2 here made the
250
+ // decoder expect a second op_response that never arrives
251
+ // whenever a single large response (e.g. op_get_segment
252
+ // with a big buffer) spans multiple TCP packets.
253
+ if (cb) {
254
+ if (lazySnapshot === undefined) {
255
+ delete cb.lazy_count;
256
+ } else {
257
+ cb.lazy_count = lazySnapshot;
258
+ }
176
259
  }
177
260
  } else {
178
261
  // Any other error (truly unknown opcode not handled above).
@@ -307,7 +390,7 @@ class Connection {
307
390
 
308
391
 
309
392
  connect(options, callback) {
310
- var pluginName = options.manager ? Const.AUTH_PLUGIN_LEGACY : options.pluginName || Const.AUTH_PLUGIN_LIST[0]; // TODO Srp for service
393
+ var pluginName = options.pluginName || Const.AUTH_PLUGIN_LIST[0];
311
394
  var msg = this._msg;
312
395
  var blr = this._blr;
313
396
 
@@ -347,10 +430,16 @@ class Connection {
347
430
  msg.addInt(Const.CONNECT_VERSION3);
348
431
  msg.addInt(Const.ARCHITECTURE_GENERIC);
349
432
  msg.addString(options.database || options.filename, Const.DEFAULT_ENCODING);
350
- msg.addInt(Const.SUPPORTED_PROTOCOL.length); // Count of Protocol version understood count.
433
+ var maxProtocols = options.maxNegotiatedProtocols !== undefined ? options.maxNegotiatedProtocols : 10;
434
+ var protocolsToSend = Const.SUPPORTED_PROTOCOL;
435
+ if (protocolsToSend.length > maxProtocols) {
436
+ protocolsToSend = protocolsToSend.slice(-maxProtocols);
437
+ }
438
+
439
+ msg.addInt(protocolsToSend.length); // Count of Protocol version understood count.
351
440
  msg.addBlr(this._blr);
352
441
 
353
- for (var protocol of Const.SUPPORTED_PROTOCOL) {
442
+ for (var protocol of protocolsToSend) {
354
443
  msg.addInt(protocol[0]); // Version
355
444
  msg.addInt(protocol[1]); // Architecture
356
445
  msg.addInt(protocol[2]); // Min type
@@ -380,21 +469,59 @@ class Connection {
380
469
  // Wire encryption: send op_crypt if SRP session key is available
381
470
  if (ret.sessionKey && ret.protocolVersion >= Const.PROTOCOL_VERSION13 &&
382
471
  options.wireCrypt !== Const.WIRE_CRYPT_DISABLE) {
383
- var keyBuf = Buffer.from(ret.sessionKey.toString(16).padStart(40, '0'), 'hex');
384
- self.sendOpCrypt('Arc4');
385
- self._socket.enableEncryption(keyBuf);
472
+ var padLen = 40;
473
+ var keyBuf = Buffer.from(ret.sessionKey.toString(16).padStart(padLen, '0'), 'hex');
474
+
475
+ var selectedPlugin = 'Arc4';
476
+ if (ret.keys) {
477
+ var serverPlugins = ret.keys.split(',').map(function(s) { return s.trim().toLowerCase(); });
478
+ var preferred = ['chacha64', 'chacha', 'arc4'];
479
+ for (var i = 0; i < preferred.length; i++) {
480
+ if (serverPlugins.indexOf(preferred[i]) !== -1) {
481
+ var mapping = {
482
+ chacha64: 'ChaCha64',
483
+ chacha: 'ChaCha',
484
+ arc4: 'Arc4'
485
+ };
486
+ selectedPlugin = mapping[preferred[i]];
487
+ break;
488
+ }
489
+ }
490
+ }
491
+
492
+ // Send op_crypt BEFORE enabling encryption (op_crypt is sent plaintext)
493
+ // The server then enables encryption from its side.
494
+ // After the op_response to op_crypt, both sides are encrypted.
495
+ self.sendOpCrypt(selectedPlugin);
496
+
497
+ if (selectedPlugin === 'Arc4') {
498
+ self._socket.enableEncryption(keyBuf, 'Arc4');
499
+ }
500
+
386
501
  self._pending.push('crypt');
387
- self._queue.push(function(cryptErr) {
502
+ self._queue.push(function(cryptErr, response) {
388
503
  if (cryptErr) {
389
504
  doError(cryptErr, callback);
390
505
  return;
391
506
  }
507
+
508
+ if (selectedPlugin !== 'Arc4') {
509
+ var iv = response && response.buffer;
510
+ if (!iv || (selectedPlugin === 'ChaCha64' && iv.length < 8) || (selectedPlugin === 'ChaCha' && iv.length < 12)) {
511
+ var errIv = new Error('Invalid or missing IV for ' + selectedPlugin + ' encryption');
512
+ doError(errIv, callback);
513
+ return;
514
+ }
515
+ self._socket.enableEncryption(keyBuf, selectedPlugin, iv);
516
+ }
517
+
392
518
  if (callback)
393
519
  callback(undefined, ret);
394
520
  });
395
521
  return;
396
522
  }
397
523
 
524
+
398
525
  if (callback)
399
526
  callback(undefined, ret);
400
527
  }
@@ -406,7 +533,7 @@ class Connection {
406
533
  }
407
534
 
408
535
 
409
- attach(options, callback, db) {
536
+ attach(options: any, callback?: any, db?: any) {
410
537
  this._lowercase_keys = options.lowercase_keys || Const.DEFAULT_LOWERCASE_KEYS;
411
538
 
412
539
  var database = options.database || options.filename;
@@ -460,6 +587,34 @@ class Connection {
460
587
  if (options.sessionTimeZone) {
461
588
  blr.addString(Const.isc_dpb_session_time_zone, options.sessionTimeZone, Const.DEFAULT_ENCODING);
462
589
  }
590
+
591
+ if (options.parallelWorkers !== undefined) {
592
+ blr.addNumeric(Const.isc_dpb_parallel_workers, options.parallelWorkers);
593
+ }
594
+
595
+ if (options.maxInlineBlobSize !== undefined) {
596
+ blr.addNumeric(Const.isc_dpb_max_inline_blob_size, options.maxInlineBlobSize);
597
+ }
598
+
599
+ // Firebird 6.0 SQL Schema parameters (Protocol 20+).
600
+ // These DPB tags configure the session's current schema and the
601
+ // schema search path for unqualified object name resolution.
602
+ if (this.accept.protocolVersion >= Const.PROTOCOL_VERSION20) {
603
+ if (options.defaultSchema) {
604
+ // Sets CURRENT_SCHEMA for the session. Equivalent to issuing
605
+ // SET SCHEMA <name> immediately after connecting.
606
+ blr.addString(Const.isc_dpb_default_schema, options.defaultSchema, Const.DEFAULT_ENCODING);
607
+ }
608
+ if (options.searchPath) {
609
+ // Comma-separated ordered schema name list, like PostgreSQL's
610
+ // search_path. Unqualified object references are resolved by
611
+ // scanning schemas in this order.
612
+ const sp = Array.isArray(options.searchPath)
613
+ ? options.searchPath.join(',')
614
+ : String(options.searchPath);
615
+ blr.addString(Const.isc_dpb_search_path, sp, Const.DEFAULT_ENCODING);
616
+ }
617
+ }
463
618
 
464
619
  msg.addInt(Const.op_attach);
465
620
  msg.addInt(0); // Database Object ID
@@ -494,11 +649,11 @@ class Connection {
494
649
 
495
650
 
496
651
  detach(callback) {
497
-
652
+
498
653
  var self = this;
499
-
654
+
500
655
  if (self._isClosed)
501
- return;
656
+ return this.throwClosed(callback);
502
657
 
503
658
  self._isUsed = false;
504
659
  self._isDetach = true;
@@ -519,6 +674,10 @@ class Connection {
519
674
 
520
675
 
521
676
  createDatabase(options, callback) {
677
+ // Mirror attach(): honour the lowercase_keys option so that db.query()
678
+ // called on a freshly-created database returns the expected column case.
679
+ this._lowercase_keys = options.lowercase_keys || Const.DEFAULT_LOWERCASE_KEYS;
680
+
522
681
  var database = options.database || options.filename;
523
682
  if (database == null || database.length === 0) {
524
683
  doError(new Error('No database specified'), callback);
@@ -566,6 +725,27 @@ class Connection {
566
725
  if (options.sessionTimeZone) {
567
726
  blr.addString(Const.isc_dpb_session_time_zone, options.sessionTimeZone, Const.DEFAULT_ENCODING);
568
727
  }
728
+
729
+ if (options.parallelWorkers !== undefined) {
730
+ blr.addNumeric(Const.isc_dpb_parallel_workers, options.parallelWorkers);
731
+ }
732
+
733
+ if (options.maxInlineBlobSize !== undefined) {
734
+ blr.addNumeric(Const.isc_dpb_max_inline_blob_size, options.maxInlineBlobSize);
735
+ }
736
+
737
+ // Firebird 6.0 SQL Schema parameters (Protocol 20+).
738
+ if (this.accept.protocolVersion >= Const.PROTOCOL_VERSION20) {
739
+ if (options.defaultSchema) {
740
+ blr.addString(Const.isc_dpb_default_schema, options.defaultSchema, Const.DEFAULT_ENCODING);
741
+ }
742
+ if (options.searchPath) {
743
+ const sp = Array.isArray(options.searchPath)
744
+ ? options.searchPath.join(',')
745
+ : String(options.searchPath);
746
+ blr.addString(Const.isc_dpb_search_path, sp, Const.DEFAULT_ENCODING);
747
+ }
748
+ }
569
749
 
570
750
  blr.addNumeric(Const.isc_dpb_sql_dialect, 3);
571
751
  blr.addNumeric(Const.isc_dpb_force_write, 1);
@@ -743,9 +923,9 @@ class Connection {
743
923
 
744
924
 
745
925
  commitRetaining(transaction, callback) {
746
-
926
+
747
927
  if (this._isClosed)
748
- throw new Error('Connection is closed.');
928
+ return this.throwClosed(callback);
749
929
 
750
930
  // for auto detach
751
931
  this._pending.push('commitRetaining');
@@ -829,7 +1009,7 @@ class Connection {
829
1009
 
830
1010
  allocateAndPrepareStatement(transaction, query, plan, callback) {
831
1011
  var self = this;
832
- var mainCallback = function(err, ret) {
1012
+ var mainCallback: any = function(err: any, ret: any) {
833
1013
  if (!err) {
834
1014
  mainCallback.response.handle = ret.handle;
835
1015
  describe(ret.buffer, mainCallback.response);
@@ -856,7 +1036,11 @@ class Connection {
856
1036
  msg.addInt(this.dbhandle);
857
1037
  mainCallback.lazy_count = 1;
858
1038
 
859
- blr.addBytes(Const.DESCRIBE);
1039
+ const describeBytes = this.accept.protocolVersion >= Const.PROTOCOL_VERSION20 ? Const.DESCRIBE_WITH_SCHEMA : Const.DESCRIBE;
1040
+ if (process.env.FIREBIRD_DEBUG) {
1041
+ console.log('[fb-debug] describeBytes:', describeBytes);
1042
+ }
1043
+ blr.addBytes(describeBytes);
860
1044
  if (plan)
861
1045
  blr.addByte(Const.isc_info_sql_get_plan);
862
1046
 
@@ -909,7 +1093,8 @@ class Connection {
909
1093
  plan = false;
910
1094
  }
911
1095
 
912
- blr.addBytes(Const.DESCRIBE);
1096
+ const describeBytes = this.accept.protocolVersion >= Const.PROTOCOL_VERSION20 ? Const.DESCRIBE_WITH_SCHEMA : Const.DESCRIBE;
1097
+ blr.addBytes(describeBytes);
913
1098
 
914
1099
  if (plan)
915
1100
  blr.addByte(Const.isc_info_sql_get_plan);
@@ -1050,6 +1235,12 @@ class Connection {
1050
1235
  value = params[i];
1051
1236
  meta = input[i];
1052
1237
 
1238
+ if (statement.connection.options && statement.connection.options.jsonAsObject && value !== null && value !== undefined && typeof value === 'object' && !(value instanceof Date) && !(value instanceof Buffer)) {
1239
+ if (typeof value.pipe !== 'function') {
1240
+ value = JSON.stringify(value);
1241
+ }
1242
+ }
1243
+
1053
1244
  if (value === null || value === undefined) {
1054
1245
  switch (meta.type) {
1055
1246
  case Const.SQL_VARYING:
@@ -1110,29 +1301,33 @@ class Connection {
1110
1301
  break;
1111
1302
 
1112
1303
  default:
1113
- switch (typeof value) {
1114
- case 'bigint':
1115
- ret[i] = new Xsql.SQLParamInt128(value);
1116
- break;
1117
- case 'number':
1118
- if (value % 1 === 0) {
1119
- if (value >= Const.MIN_INT && value <= Const.MAX_INT)
1120
- ret[i] = new Xsql.SQLParamInt(value);
1121
- else
1122
- ret[i] = new Xsql.SQLParamInt64(value);
1123
- } else
1124
- ret[i] = new Xsql.SQLParamDouble(value);
1125
- break;
1126
- case 'string':
1127
- ret[i] = new Xsql.SQLParamString(value);
1128
- break;
1129
- case 'boolean':
1130
- ret[i] = new Xsql.SQLParamBool(value);
1131
- break;
1132
- default:
1133
- //throw new Error('Unexpected parametter: ' + JSON.stringify(params) + ' - ' + JSON.stringify(input));
1134
- ret[i] = new Xsql.SQLParamString(value.toString());
1135
- break;
1304
+ if (Buffer.isBuffer(value)) {
1305
+ ret[i] = new Xsql.SQLParamBuffer(value);
1306
+ } else {
1307
+ switch (typeof value) {
1308
+ case 'bigint':
1309
+ ret[i] = new Xsql.SQLParamInt128(value);
1310
+ break;
1311
+ case 'number':
1312
+ if (value % 1 === 0) {
1313
+ if (value >= Const.MIN_INT && value <= Const.MAX_INT)
1314
+ ret[i] = new Xsql.SQLParamInt(value);
1315
+ else
1316
+ ret[i] = new Xsql.SQLParamInt64(value);
1317
+ } else
1318
+ ret[i] = new Xsql.SQLParamDouble(value);
1319
+ break;
1320
+ case 'string':
1321
+ ret[i] = new Xsql.SQLParamString(value);
1322
+ break;
1323
+ case 'boolean':
1324
+ ret[i] = new Xsql.SQLParamBool(value);
1325
+ break;
1326
+ default:
1327
+ //throw new Error('Unexpected parametter: ' + JSON.stringify(params) + ' - ' + JSON.stringify(input));
1328
+ ret[i] = new Xsql.SQLParamString(value.toString());
1329
+ break;
1330
+ }
1136
1331
  }
1137
1332
  step(i + 1);
1138
1333
  }
@@ -1147,10 +1342,39 @@ class Connection {
1147
1342
  if (input.length) {
1148
1343
 
1149
1344
  if (!(params instanceof Array)) {
1150
- if (params !== undefined)
1345
+ if (params !== undefined && typeof params === 'object' && params !== null) {
1346
+ var mappedParams = [];
1347
+ for (var i = 0; i < input.length; i++) {
1348
+ mappedParams.push(undefined);
1349
+ }
1350
+ var matchedCount = 0;
1351
+ var nameMap = {};
1352
+ for (var i = 0; i < input.length; i++) {
1353
+ var name = input[i].alias || input[i].field;
1354
+ if (name) {
1355
+ nameMap[name.toUpperCase()] = i;
1356
+ }
1357
+ }
1358
+ for (var key in params) {
1359
+ if (Object.prototype.hasOwnProperty.call(params, key)) {
1360
+ var cleanKey = key.startsWith(':') ? key.substring(1) : key;
1361
+ var index = nameMap[cleanKey.toUpperCase()];
1362
+ if (index !== undefined) {
1363
+ mappedParams[index] = params[key];
1364
+ matchedCount++;
1365
+ }
1366
+ }
1367
+ }
1368
+ if (matchedCount > 0) {
1369
+ params = mappedParams;
1370
+ } else {
1371
+ params = [params];
1372
+ }
1373
+ } else if (params !== undefined) {
1151
1374
  params = [params];
1152
- else
1375
+ } else {
1153
1376
  params = [];
1377
+ }
1154
1378
  }
1155
1379
 
1156
1380
  if (params.length !== input.length) {
@@ -1170,7 +1394,7 @@ class Connection {
1170
1394
  }
1171
1395
 
1172
1396
 
1173
- sendExecute(op, statement, transaction, callback, parameters) {
1397
+ sendExecute(op: number, statement: any, transaction: any, callback: any, parameters?: any[]) {
1174
1398
  var msg = this._msg;
1175
1399
  var blr = this._blr;
1176
1400
  msg.pos = 0;
@@ -1191,7 +1415,7 @@ class Connection {
1191
1415
  var nullBits = new BitSet();
1192
1416
 
1193
1417
  for (var i = 0; i < parameters.length; i++) {
1194
- nullBits.set(i, (parameters[i].value === null) & 1);
1418
+ nullBits.set(i, parameters[i].value === null ? 1 : 0);
1195
1419
  }
1196
1420
 
1197
1421
  var nullBuffer = nullBits.toBuffer();
@@ -1241,6 +1465,14 @@ class Connection {
1241
1465
  // TODO impl statement timout
1242
1466
  msg.addInt(statement.options?.timeout || 0); // p_sqldata_timeout
1243
1467
  }
1468
+
1469
+ if (this.accept.protocolVersion >= Const.PROTOCOL_VERSION18) {
1470
+ msg.addInt(statement.options?.scrollable ? 1 : 0); // p_sqldata_cursor_flags (1 = CURSOR_TYPE_SCROLLABLE)
1471
+ }
1472
+
1473
+ if (this.accept.protocolVersion >= Const.PROTOCOL_VERSION19) {
1474
+ msg.addInt(statement.options?.maxInlineBlobSize !== undefined ? statement.options.maxInlineBlobSize : (this.options?.maxInlineBlobSize || 0)); // p_sqldata_inline_blob_size
1475
+ }
1244
1476
 
1245
1477
  callback.statement = statement;
1246
1478
  this._queueEvent(callback);
@@ -1274,6 +1506,57 @@ class Connection {
1274
1506
  }
1275
1507
 
1276
1508
 
1509
+ fetchScroll(statement, transaction, direction, offset, count, callback) {
1510
+ if (typeof count === 'function') {
1511
+ callback = count;
1512
+ count = undefined;
1513
+ }
1514
+ if (typeof offset === 'function') {
1515
+ callback = offset;
1516
+ offset = undefined;
1517
+ count = undefined;
1518
+ }
1519
+
1520
+ var msg = this._msg;
1521
+ var blr = this._blr;
1522
+
1523
+ msg.pos = 0;
1524
+ blr.pos = 0;
1525
+
1526
+ let dirInt = 0;
1527
+ if (typeof direction === 'number') {
1528
+ dirInt = direction;
1529
+ } else if (typeof direction === 'string') {
1530
+ const dirUpper = direction.toUpperCase();
1531
+ switch (dirUpper) {
1532
+ case 'NEXT': dirInt = 0; break;
1533
+ case 'PRIOR': dirInt = 1; break;
1534
+ case 'FIRST': dirInt = 2; break;
1535
+ case 'LAST': dirInt = 3; break;
1536
+ case 'ABSOLUTE': dirInt = 4; break;
1537
+ case 'RELATIVE': dirInt = 5; break;
1538
+ default:
1539
+ throw new Error('Invalid fetch direction: ' + direction);
1540
+ }
1541
+ }
1542
+
1543
+ const offsetVal = offset || 0;
1544
+ const fetchCount = count || 1;
1545
+
1546
+ msg.addInt(Const.op_fetch_scroll);
1547
+ msg.addInt(statement.handle);
1548
+ CalcBlr(blr, statement.output);
1549
+ msg.addBlr(blr);
1550
+ msg.addInt(0); // message number
1551
+ msg.addInt(fetchCount); // fetch count
1552
+ msg.addInt(dirInt); // fetch operation
1553
+ msg.addInt(offsetVal); // fetch position (offset)
1554
+
1555
+ callback.statement = statement;
1556
+ this._queueEvent(callback);
1557
+ }
1558
+
1559
+
1277
1560
  fetchAll(statement, transaction, callback) {
1278
1561
  const self = this;
1279
1562
  const custom = statement.options || {};
@@ -1287,12 +1570,25 @@ class Connection {
1287
1570
  }
1288
1571
 
1289
1572
  if (ret && ret.data && ret.data.length) {
1290
- const arrPromise = (ret.arrBlob || []).map((value) => value(transaction));
1573
+ // Read blobs sequentially instead of in parallel to avoid
1574
+ // exceeding Firebird's per-connection open-blob-handle limit,
1575
+ // which causes a server-side deadlock when many rows contain
1576
+ // BLOBs and blobAsText is true. See issue #387.
1577
+ const arrBlobFns = ret.arrBlob || [];
1578
+ const readBlobsSequentially = (index, results) => {
1579
+ if (index >= arrBlobFns.length) {
1580
+ return Promise.resolve(results);
1581
+ }
1582
+ return arrBlobFns[index](transaction).then((v) => {
1583
+ results.push(v);
1584
+ return readBlobsSequentially(index + 1, results);
1585
+ });
1586
+ };
1291
1587
 
1292
- Promise.all(arrPromise).then((arrBlob) => {
1588
+ readBlobsSequentially(0, []).then((arrBlob) => {
1293
1589
  for (let i = 0; i < arrBlob.length; i++) {
1294
1590
  const blob = arrBlob[i];
1295
- ret.data[blob.row][blob.column] = blob.value;
1591
+ ret.data[blob.row][blob.column] = parseValueIfJson(blob.value, statement.connection.options);
1296
1592
  }
1297
1593
 
1298
1594
  doSynchronousLoop(ret.data, (row, _i, next) => {
@@ -1354,7 +1650,7 @@ class Connection {
1354
1650
  msg.pos = 0;
1355
1651
  msg.addInt(Const.op_get_segment);
1356
1652
  msg.addInt(blob.handle);
1357
- msg.addInt(1024); // buffer length
1653
+ msg.addInt(this.options.blobReadChunkSize || 1024); // buffer length (max 65535)
1358
1654
  msg.addInt(0); // ???
1359
1655
  this._queueEvent(callback);
1360
1656
  }
@@ -1386,7 +1682,7 @@ class Connection {
1386
1682
  }
1387
1683
 
1388
1684
 
1389
- svcattach(options, callback, svc) {
1685
+ svcattach(options: any, callback?: any, svc?: any) {
1390
1686
  this._lowercase_keys = options.lowercase_keys || Const.DEFAULT_LOWERCASE_KEYS;
1391
1687
  var database = options.database || options.filename;
1392
1688
  var user = options.user || Const.DEFAULT_USER;
@@ -1485,9 +1781,11 @@ class Connection {
1485
1781
 
1486
1782
  svcdetach(callback) {
1487
1783
  var self = this;
1488
-
1489
- if (self._isClosed)
1784
+
1785
+ if (self._isClosed) {
1786
+ doError(new Error('Connection is closed.'), callback);
1490
1787
  return;
1788
+ }
1491
1789
 
1492
1790
  self._isUsed = false;
1493
1791
  self._isDetach = true;
@@ -1507,7 +1805,13 @@ class Connection {
1507
1805
 
1508
1806
 
1509
1807
 
1510
- auxConnection(callback) {
1808
+ auxConnection(eventid, callback) {
1809
+ if (typeof eventid === 'function') {
1810
+ // Preserve the older auxConnection(callback) call shape; plain
1811
+ // auxiliary connections historically used event id 0.
1812
+ callback = eventid;
1813
+ eventid = 0;
1814
+ }
1511
1815
  var self = this;
1512
1816
  if (self._isClosed)
1513
1817
  return this.throwClosed(callback);
@@ -1516,10 +1820,10 @@ class Connection {
1516
1820
  msg.addInt(Const.op_connect_request);
1517
1821
  msg.addInt(1); // async
1518
1822
  msg.addInt(self.dbhandle);
1519
- msg.addInt(0);
1823
+ msg.addInt(eventid);
1520
1824
  if (process.env.FIREBIRD_DEBUG) {
1521
- console.log('[fb-debug] auxConnection: sending op_connect_request(53) dbhandle=%d queue_before=%d xdr_saved=%s',
1522
- self.dbhandle, self._queue.length, Boolean(self._xdr));
1825
+ console.log('[fb-debug] auxConnection: sending op_connect_request(53) dbhandle=%d eventid=%d queue_before=%d xdr_saved=%s',
1826
+ self.dbhandle, eventid, self._queue.length, Boolean(self._xdr));
1523
1827
  }
1524
1828
  function cb(err, ret) {
1525
1829
 
@@ -1561,7 +1865,7 @@ class Connection {
1561
1865
  // prepare EPB
1562
1866
  blr.addByte(1) // epb_version
1563
1867
  for (var event in events) {
1564
- var event_buffer = Buffer.from(event, 'UTF8');
1868
+ var event_buffer = Buffer.from(event, 'utf8');
1565
1869
  blr.addByte(event_buffer.length);
1566
1870
  blr.addBytes(event_buffer);
1567
1871
  blr.addInt32(events[event]);
@@ -1613,10 +1917,30 @@ const opcodeNames = Object.fromEntries(
1613
1917
  Object.entries(Const).filter(([k]) => k.startsWith('op_')).map(([k, v]) => [v, k])
1614
1918
  );
1615
1919
 
1616
- function decodeResponse(data, callback, cnx, lowercase_keys, cb) {
1920
+ function decodeResponse(data: any, callback: any, cnx: any, lowercase_keys: any, cb: (err?: any, obj?: any) => void) {
1617
1921
  try {
1618
1922
  do {
1619
1923
  var r = data.r || data.readInt();
1924
+ if (data.r) {
1925
+ data.r = null;
1926
+ }
1927
+ if (r === Const.op_dummy) {
1928
+ continue;
1929
+ }
1930
+ if (r === Const.op_inline_blob) {
1931
+ if (process.env.FIREBIRD_DEBUG) {
1932
+ console.log('[fb-debug] decodeResponse: opcode=op_inline_blob');
1933
+ }
1934
+ var tran_id = data.readInt();
1935
+ var blob_id = data.readQuad();
1936
+ var blob_data = data.readArray();
1937
+ if (!cnx._inlineBlobs) {
1938
+ cnx._inlineBlobs = new Map();
1939
+ }
1940
+ const cacheKey = `${blob_id.high}:${blob_id.low}`;
1941
+ cnx._inlineBlobs.set(cacheKey, blob_data);
1942
+ r = Const.op_dummy; // Continue loop to read next opcode
1943
+ }
1620
1944
  } while (r === Const.op_dummy);
1621
1945
 
1622
1946
  if (process.env.FIREBIRD_DEBUG) {
@@ -1718,7 +2042,7 @@ function decodeResponse(data, callback, cnx, lowercase_keys, cb) {
1718
2042
  _xdrpos = data.pos;
1719
2043
  const key = custom.asObject ? data.fcols[data.fcolumn] : data.fcolumn;
1720
2044
  const row = data.frows.length;
1721
- let value = item.decode(data, lowerV13);
2045
+ let value = item.decode(data, lowerV13, cnx.options);
1722
2046
 
1723
2047
  if (item.type === Const.SQL_BLOB && value !== null) {
1724
2048
  if (item.subType === Const.isc_blob_text && cnx.options.blobAsText) {
@@ -1729,7 +2053,7 @@ function decodeResponse(data, callback, cnx, lowercase_keys, cb) {
1729
2053
  }
1730
2054
  }
1731
2055
 
1732
- data.frow[key] = value;
2056
+ data.frow[key] = parseValueIfJson(value, cnx.options);
1733
2057
  } catch (e) {
1734
2058
  // uncomplete packet read
1735
2059
  data.pos = _xdrpos;
@@ -1783,7 +2107,7 @@ function decodeResponse(data, callback, cnx, lowercase_keys, cb) {
1783
2107
  case Const.op_accept:
1784
2108
  case Const.op_cond_accept:
1785
2109
  case Const.op_accept_data:
1786
- let accept = {
2110
+ let accept: any = {
1787
2111
  protocolVersion: data.readInt(),
1788
2112
  protocolArchitecture: data.readInt(),
1789
2113
  protocolMinimumType: data.readInt(),
@@ -1805,29 +2129,39 @@ function decodeResponse(data, callback, cnx, lowercase_keys, cb) {
1805
2129
  accept.pluginName = data.readString(Const.DEFAULT_ENCODING);
1806
2130
  var is_authenticated = data.readInt();
1807
2131
  var keys = data.readString(Const.DEFAULT_ENCODING); // keys
2132
+ accept.keys = keys;
1808
2133
 
1809
2134
  if (is_authenticated === 0) {
1810
2135
  if (cnx.options.pluginName && cnx.options.pluginName !== accept.pluginName) {
1811
- doError(new Error('Server don\'t accept plugin : ' + cnx.options.pluginName + ', but support : ' + accept.pluginName), callback);
2136
+ var errPlugin = new Error('Server don\'t accept plugin : ' + cnx.options.pluginName + ', but support : ' + accept.pluginName);
2137
+ doError(errPlugin, callback);
2138
+ return cb(errPlugin);
2139
+ }
2140
+
2141
+ if (Const.AUTH_PLUGIN_SRP_LIST.indexOf(accept.pluginName) !== -1 && !cnx.clientKeys) {
2142
+ var errPlugin = new Error('Server accepted plugin : ' + accept.pluginName + ', but client did not initialize SRP keys');
2143
+ doError(errPlugin, callback);
2144
+ return cb(errPlugin);
1812
2145
  }
1813
2146
 
1814
2147
  if (Const.AUTH_PLUGIN_SRP_LIST.indexOf(accept.pluginName) !== -1) {
1815
2148
  var crypto = {
1816
2149
  Srp: 'sha1',
1817
- Srp256: 'sha256'
2150
+ Srp256: 'sha256',
2151
+ Srp384: 'sha384',
2152
+ Srp512: 'sha512'
1818
2153
  };
1819
2154
  accept.srpAlgo = crypto[accept.pluginName];
1820
2155
 
1821
- // TODO : Fallback Srp256 to Srp ?
1822
- /*if (!d.buffer) {
2156
+ if (!d.buffer) {
2157
+ cnx._pendingAccept = accept;
1823
2158
  cnx.sendOpContAuth(
1824
2159
  cnx.clientKeys.public.toString(16),
1825
- DEFAULT_ENCODING,
2160
+ Const.DEFAULT_ENCODING,
1826
2161
  accept.pluginName
1827
2162
  );
1828
-
1829
- return cb(new Error('login'));
1830
- }*/
2163
+ return;
2164
+ }
1831
2165
 
1832
2166
  // Check buffer contains salt
1833
2167
  var saltLen = d.buffer.readUInt16LE(0);
@@ -1837,17 +2171,27 @@ function decodeResponse(data, callback, cnx, lowercase_keys, cb) {
1837
2171
 
1838
2172
  // Check buffer contains key
1839
2173
  var keyLen = d.buffer.readUInt16LE(saltLen + 2);
1840
- var keyStart = saltLen + 4;
1841
- if (d.buffer.length - keyStart !== keyLen) {
1842
- console.log('key error'); // TODO : Throw error
2174
+ if (d.buffer.length < saltLen + 4) {
2175
+ var errBuf = new Error('Invalid buffer size for ' + accept.pluginName + ' login');
2176
+ doError(errBuf, callback);
2177
+ return cb(errBuf);
1843
2178
  }
2179
+ var keyStart = (saltLen + 2 + 3) & ~3;
1844
2180
 
1845
2181
  // Server keys
1846
2182
  cnx.serverKeys = {
1847
2183
  salt: d.buffer.slice(2, saltLen + 2).toString('utf8'),
1848
- public: BigInt('0x' + d.buffer.slice(keyStart, d.buffer.length).toString('utf8'))
2184
+ public: BigInt('0x' + d.buffer.slice(keyStart, d.buffer.length).toString('utf8')),
2185
+ pluginName: accept.pluginName
1849
2186
  };
1850
2187
 
2188
+ console.log('--- DEBUG SRP Handshake ---');
2189
+ console.log('salt:', cnx.serverKeys.salt);
2190
+ console.log('server public key:', cnx.serverKeys.public.toString(16));
2191
+ console.log('client public key:', cnx.clientKeys.public.toString(16));
2192
+ console.log('client private key:', cnx.clientKeys.private.toString(16));
2193
+ console.log('hashAlgo:', accept.srpAlgo);
2194
+
1851
2195
  const _t1 = Date.now();
1852
2196
  var proof = srp.clientProof(
1853
2197
  cnx.options.user.toUpperCase(),
@@ -1858,6 +2202,10 @@ function decodeResponse(data, callback, cnx, lowercase_keys, cb) {
1858
2202
  cnx.clientKeys.private,
1859
2203
  accept.srpAlgo
1860
2204
  );
2205
+
2206
+ console.log('client proof M1:', proof.authData.toString(16));
2207
+ console.log('client session key K:', proof.clientSessionKey.toString(16));
2208
+
1861
2209
  if (process.env.FIREBIRD_DEBUG) {
1862
2210
  console.log('[fb-debug] srp.clientProof(%s): %dms', accept.srpAlgo, Date.now() - _t1);
1863
2211
  }
@@ -1917,14 +2265,88 @@ function decodeResponse(data, callback, cnx, lowercase_keys, cb) {
1917
2265
 
1918
2266
  // During SRP mutual authentication, the server sends op_cont_auth
1919
2267
  // with its proof (M2) after receiving the client's proof (M1).
1920
- // When we have an active auth exchange for this plugin, just wait
1921
- // for the subsequent op_accept instead of treating it as an error.
1922
- if (cnx._pendingAccept && cnx._pendingAccept.pluginName === pluginName) {
1923
- if (process.env.FIREBIRD_DEBUG) {
1924
- console.log('[fb-debug] auth: server SRP proof (M2) received, waiting for op_accept t=%dms',
1925
- cnx._authStartTime ? Date.now() - cnx._authStartTime : -1);
2268
+ // When we have an active auth exchange for this plugin:
2269
+ if (cnx._pendingAccept && (cnx._pendingAccept.pluginName === pluginName || Const.AUTH_PLUGIN_SRP_LIST.indexOf(pluginName) !== -1)) {
2270
+ if (cnx._pendingAccept.pluginName !== pluginName) {
2271
+ cnx._pendingAccept.pluginName = pluginName;
2272
+ }
2273
+ // The server can switch SRP hash variants mid-handshake: after
2274
+ // the client answers with e.g. Srp256, some accounts make the
2275
+ // server come back with a fresh op_cont_auth naming a different
2276
+ // SRP plugin (e.g. plain Srp/sha1), reusing the same salt/B.
2277
+ // That must be treated as a brand-new challenge - recomputing
2278
+ // the proof with the new plugin's hash algorithm - rather than
2279
+ // as the server's M2 proof, otherwise the client silently waits
2280
+ // forever for an op_accept the server will never send (#254).
2281
+ if (!cnx.serverKeys || cnx.serverKeys.pluginName !== pluginName) {
2282
+ // Check buffer contains salt
2283
+ var saltLen = d.buffer.readUInt16LE(0);
2284
+ if (saltLen > 32 * 2) {
2285
+ console.log('salt to long'); // TODO : Throw error
2286
+ }
2287
+
2288
+ // Check buffer contains key
2289
+ var keyLen = d.buffer.readUInt16LE(saltLen + 2);
2290
+ if (d.buffer.length < saltLen + 4) {
2291
+ var errBuf = new Error('Invalid buffer size for ' + pluginName + ' login');
2292
+ doError(errBuf, callback);
2293
+ return cb(errBuf);
2294
+ }
2295
+ var keyStart = (saltLen + 2 + 3) & ~3;
2296
+
2297
+ // Server keys
2298
+ cnx.serverKeys = {
2299
+ salt: d.buffer.slice(2, saltLen + 2).toString('utf8'),
2300
+ public: BigInt('0x' + d.buffer.slice(keyStart, d.buffer.length).toString('utf8')),
2301
+ pluginName: pluginName
2302
+ };
2303
+
2304
+ var crypto = {
2305
+ Srp: 'sha1',
2306
+ Srp256: 'sha256',
2307
+ Srp384: 'sha384',
2308
+ Srp512: 'sha512'
2309
+ };
2310
+ var srpAlgo = crypto[pluginName];
2311
+
2312
+ console.log('--- DEBUG SRP Handshake ---');
2313
+ console.log('salt:', cnx.serverKeys.salt);
2314
+ console.log('server public key:', cnx.serverKeys.public.toString(16));
2315
+ console.log('client public key:', cnx.clientKeys.public.toString(16));
2316
+ console.log('client private key:', cnx.clientKeys.private.toString(16));
2317
+ console.log('hashAlgo:', srpAlgo);
2318
+
2319
+ const _t1 = Date.now();
2320
+ var proof = srp.clientProof(
2321
+ cnx.options.user.toUpperCase(),
2322
+ cnx.options.password,
2323
+ cnx.serverKeys.salt,
2324
+ cnx.clientKeys.public,
2325
+ cnx.serverKeys.public,
2326
+ cnx.clientKeys.private,
2327
+ srpAlgo
2328
+ );
2329
+
2330
+ if (process.env.FIREBIRD_DEBUG) {
2331
+ console.log('[fb-debug] srp.clientProof(%s): %dms', srpAlgo, Date.now() - _t1);
2332
+ }
2333
+
2334
+ cnx._pendingAccept.authData = proof.authData.toString(16);
2335
+ cnx._pendingAccept.sessionKey = proof.clientSessionKey;
2336
+
2337
+ cnx.sendOpContAuth(
2338
+ cnx._pendingAccept.authData,
2339
+ Const.DEFAULT_ENCODING,
2340
+ pluginName
2341
+ );
2342
+ return; // wait for server SRP proof (M2) and/or op_accept
2343
+ } else {
2344
+ if (process.env.FIREBIRD_DEBUG) {
2345
+ console.log('[fb-debug] auth: server SRP proof (M2) received, waiting for op_accept t=%dms',
2346
+ cnx._authStartTime ? Date.now() - cnx._authStartTime : -1);
2347
+ }
2348
+ return; // Server SRP proof received - wait for op_accept
1926
2349
  }
1927
- return; // Server SRP proof received - wait for op_accept
1928
2350
  }
1929
2351
 
1930
2352
  // Firebird 4/5 (protocols 16/17) chained-auth: after the client sends
@@ -1976,6 +2398,7 @@ function decodeResponse(data, callback, cnx, lowercase_keys, cb) {
1976
2398
  // Database encryption key callback
1977
2399
  // Read server data (plugin data sent by server)
1978
2400
  var serverPluginData = data.readArray();
2401
+ data.readInt(); // p_cc_reply
1979
2402
 
1980
2403
  // Get client response from dbCryptConfig option
1981
2404
  var clientPluginData = parseDbCryptConfig(cnx.options.dbCryptConfig);
@@ -2055,7 +2478,7 @@ function decodeResponse(data, callback, cnx, lowercase_keys, cb) {
2055
2478
  }
2056
2479
  }
2057
2480
 
2058
- function parseOpResponse(data, response, cb) {
2481
+ function parseOpResponse(data: any, response: any, cb?: (err?: any, response?: any) => void) {
2059
2482
  var handle = data.readInt();
2060
2483
 
2061
2484
  if (!response.handle) {
@@ -2072,7 +2495,7 @@ function parseOpResponse(data, response, cb) {
2072
2495
  response.buffer = buf;
2073
2496
  }
2074
2497
 
2075
- var num, op, item = {};
2498
+ var num: any, op: any, item: any = {};
2076
2499
  while (true) {
2077
2500
  op = data.readInt();
2078
2501
 
@@ -2129,10 +2552,10 @@ function parseOpResponse(data, response, cb) {
2129
2552
  }
2130
2553
  }
2131
2554
 
2132
- function describe(buff, statement) {
2555
+ function describe(buff: Buffer, statement: any) {
2133
2556
  var br = new BlrReader(buff);
2134
- var parameters = null;
2135
- var type, param;
2557
+ var parameters: any = null;
2558
+ var type: any, param: any;
2136
2559
 
2137
2560
  while (br.pos < br.buffer.length) {
2138
2561
  switch (br.readByteCode()) {
@@ -2215,6 +2638,12 @@ function describe(buff, statement) {
2215
2638
  case Const.isc_info_sql_relation:
2216
2639
  param.relation = br.readString(Const.DEFAULT_ENCODING);
2217
2640
  break;
2641
+ case Const.isc_info_sql_relation_schema:
2642
+ // Firebird 6.0 (Protocol 20+): schema that owns the
2643
+ // source relation. Empty string means the default
2644
+ // (PUBLIC) schema or a non-schema-aware server.
2645
+ param.relationSchema = br.readString(Const.DEFAULT_ENCODING);
2646
+ break;
2218
2647
  case Const.isc_info_sql_owner:
2219
2648
  param.owner = br.readString(Const.DEFAULT_ENCODING);
2220
2649
  break;
@@ -2233,9 +2662,24 @@ function describe(buff, statement) {
2233
2662
  }
2234
2663
  }
2235
2664
  }
2665
+
2666
+ function unpackCharSetCollation(params: any[]) {
2667
+ if (!params) return;
2668
+ for (let i = 0; i < params.length; i++) {
2669
+ const p = params[i];
2670
+ if (p && (p.type === Const.SQL_TEXT || p.type === Const.SQL_VARYING)) {
2671
+ if (p.subType !== undefined) {
2672
+ p.charSetId = p.subType & 0xFF;
2673
+ p.collationId = p.subType >> 8;
2674
+ }
2675
+ }
2676
+ }
2677
+ }
2678
+ unpackCharSetCollation(statement.input);
2679
+ unpackCharSetCollation(statement.output);
2236
2680
  }
2237
2681
 
2238
- function CalcBlr(blr, xsqlda) {
2682
+ function CalcBlr(blr: BlrWriter, xsqlda: any[]) {
2239
2683
  blr.addBytes([Const.blr_version5, Const.blr_begin, Const.blr_message, 0]); // + message number
2240
2684
  blr.addWord(xsqlda.length * 2);
2241
2685
 
@@ -2249,10 +2693,17 @@ function CalcBlr(blr, xsqlda) {
2249
2693
  blr.addByte(Const.blr_eoc);
2250
2694
  }
2251
2695
 
2252
- function fetch_blob_async_transaction(statement, id, column, row) {
2696
+ function fetch_blob_async_transaction(statement: any, id: any, column: any, row: any) {
2253
2697
  const infoValue = { row, column, value: '' };
2254
2698
 
2255
2699
  return (transactionArg) => {
2700
+ const cacheKey = `${id.high}:${id.low}`;
2701
+ if (statement.connection._inlineBlobs && statement.connection._inlineBlobs.has(cacheKey)) {
2702
+ const data = statement.connection._inlineBlobs.get(cacheKey);
2703
+ infoValue.value = data ? data.toString(Const.DEFAULT_ENCODING) : '';
2704
+ return Promise.resolve(infoValue);
2705
+ }
2706
+
2256
2707
  const singleTransaction = transactionArg === undefined;
2257
2708
 
2258
2709
  let promiseTransaction;
@@ -2324,11 +2775,11 @@ function fetch_blob_async_transaction(statement, id, column, row) {
2324
2775
  };
2325
2776
  }
2326
2777
 
2327
- function fetch_blob_async(statement, id, name, row) {
2778
+ function fetch_blob_async(statement: any, id: any, name: any, row: any) {
2328
2779
  const cbTransaction = (transaction, close, callback) => {
2329
2780
  statement.connection._pending.push('openBlob');
2330
2781
  statement.connection.openBlob(id, transaction, (err, blob) => {
2331
- let e = new Events.EventEmitter();
2782
+ let e: any = new Events.EventEmitter();
2332
2783
 
2333
2784
  e.pipe = (stream) => {
2334
2785
  e.on('data', (chunk) => {
@@ -2390,6 +2841,32 @@ function fetch_blob_async(statement, id, name, row) {
2390
2841
  return (transaction, callback) => {
2391
2842
  // callback(error, nameField, eventEmitter, row)
2392
2843
  const singleTransaction = callback === undefined;
2844
+ const actualCallback = singleTransaction ? transaction : callback;
2845
+
2846
+ const cacheKey = `${id.high}:${id.low}`;
2847
+ if (statement.connection._inlineBlobs && statement.connection._inlineBlobs.has(cacheKey)) {
2848
+ const data = statement.connection._inlineBlobs.get(cacheKey);
2849
+ let e: any = new Events.EventEmitter();
2850
+ e.pipe = (stream) => {
2851
+ e.on('data', (chunk) => {
2852
+ stream.write(chunk);
2853
+ });
2854
+ e.on('end', () => {
2855
+ stream.end();
2856
+ });
2857
+ };
2858
+
2859
+ actualCallback(null, name, e, row);
2860
+
2861
+ setImmediate(() => {
2862
+ if (data) {
2863
+ e.emit('data', data);
2864
+ }
2865
+ e.emit('end');
2866
+ });
2867
+ return;
2868
+ }
2869
+
2393
2870
  if (singleTransaction) {
2394
2871
  callback = transaction;
2395
2872
  statement.connection.startTransaction(Const.ISOLATION_READ_UNCOMMITTED, (err, transaction) => {
@@ -2405,7 +2882,7 @@ function fetch_blob_async(statement, id, name, row) {
2405
2882
  };
2406
2883
  }
2407
2884
 
2408
- function doSynchronousLoop(data, processData, done) {
2885
+ function doSynchronousLoop(data: any[], processData: (row: any, index: number, next: (err?: any) => void) => void, done: (err?: any) => void) {
2409
2886
  if (!data || !data.length) {
2410
2887
  done();
2411
2888
  return;
@@ -2430,9 +2907,9 @@ function doSynchronousLoop(data, processData, done) {
2430
2907
  loop(0);
2431
2908
  }
2432
2909
 
2433
- function executeStreamRow(custom, row, index, output, next) {
2910
+ function executeStreamRow(custom: any, row: any, index: number, output: any, next: (err?: any) => void) {
2434
2911
  let done = false;
2435
- const finish = (err) => {
2912
+ const finish = (err?: any) => {
2436
2913
  if (done) {
2437
2914
  return;
2438
2915
  }
@@ -2456,7 +2933,7 @@ function executeStreamRow(custom, row, index, output, next) {
2456
2933
  }
2457
2934
  }
2458
2935
 
2459
- function bufferReader(buffer, max, writer, cb, beg, end) {
2936
+ function bufferReader(buffer: Buffer, max: number, writer: (b: Buffer, next: () => void) => void, cb: () => void, beg?: number, end?: number) {
2460
2937
 
2461
2938
  if (!beg)
2462
2939
  beg = 0;
@@ -2487,7 +2964,7 @@ function bufferReader(buffer, max, writer, cb, beg, end) {
2487
2964
  * - plain string - encodes as UTF-8
2488
2965
  * - undefined/null/empty - returns empty buffer
2489
2966
  */
2490
- function parseDbCryptConfig(config) {
2967
+ function parseDbCryptConfig(config: string | null | undefined): Buffer {
2491
2968
  if (!config) {
2492
2969
  return Buffer.alloc(0);
2493
2970
  }
@@ -2507,4 +2984,9 @@ function parseDbCryptConfig(config) {
2507
2984
  return Buffer.from(config, 'utf8');
2508
2985
  }
2509
2986
 
2510
- module.exports = Connection;
2987
+ Connection.decodeResponse = decodeResponse;
2988
+ Connection.fetch_blob_async_transaction = fetch_blob_async_transaction;
2989
+ Connection.fetch_blob_async = fetch_blob_async;
2990
+ Connection.parseValueIfJson = parseValueIfJson;
2991
+ Connection.describe = describe;
2992
+ export = Connection;