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.
- package/README.md +574 -20
- package/lib/callback.d.ts +20 -0
- package/lib/callback.js +6 -16
- package/lib/firebird.msg.json +1370 -1370
- package/lib/gdscodes.d.ts +1519 -1520
- package/lib/gdscodes.js +5 -5
- package/lib/ieee754-decimal.d.ts +38 -0
- package/lib/ieee754-decimal.js +71 -118
- package/lib/index.d.ts +37 -338
- package/lib/index.js +97 -86
- package/lib/messages.d.ts +6 -0
- package/lib/messages.js +135 -162
- package/lib/pool.d.ts +23 -0
- package/lib/pool.js +34 -47
- package/lib/srp.d.ts +51 -0
- package/lib/srp.js +62 -72
- package/lib/types.d.ts +375 -0
- package/lib/types.js +22 -0
- package/lib/unix-crypt.d.ts +1 -0
- package/lib/unix-crypt.js +126 -152
- package/lib/utils.d.ts +16 -0
- package/lib/utils.js +24 -48
- package/lib/wire/connection.d.ts +100 -0
- package/lib/wire/connection.js +1029 -1009
- package/lib/wire/const.d.ts +567 -0
- package/lib/wire/const.js +620 -599
- package/lib/wire/database.d.ts +57 -0
- package/lib/wire/database.js +111 -108
- package/lib/wire/eventConnection.d.ts +16 -0
- package/lib/wire/eventConnection.js +29 -31
- package/lib/wire/fbEventManager.d.ts +45 -0
- package/lib/wire/fbEventManager.js +38 -57
- package/lib/wire/serialize.d.ts +97 -0
- package/lib/wire/serialize.js +42 -141
- package/lib/wire/service.d.ts +66 -0
- package/lib/wire/service.js +374 -401
- package/lib/wire/socket.d.ts +59 -0
- package/lib/wire/socket.js +79 -40
- package/lib/wire/statement.d.ts +25 -0
- package/lib/wire/statement.js +13 -9
- package/lib/wire/transaction.d.ts +21 -0
- package/lib/wire/transaction.js +33 -61
- package/lib/wire/xsqlvar.d.ts +175 -0
- package/lib/wire/xsqlvar.js +169 -269
- package/package.json +12 -4
- package/src/callback.ts +55 -0
- package/{poc/node_modules/node-firebird/lib/gdscodes.js → src/gdscodes.ts} +3 -3
- package/{poc/node_modules/node-firebird/lib/ieee754-decimal.js → src/ieee754-decimal.ts} +7 -14
- package/src/index.ts +157 -0
- package/{poc/node_modules/node-firebird/lib/messages.js → src/messages.ts} +163 -162
- package/src/pool.ts +209 -0
- package/{poc/node_modules/node-firebird/lib/srp.js → src/srp.ts} +74 -33
- package/src/types.ts +419 -0
- package/{poc/node_modules/node-firebird/lib/unix-crypt.js → src/unix-crypt.ts} +6 -12
- package/{poc/node_modules/node-firebird/lib/utils.js → src/utils.ts} +11 -20
- package/{poc/node_modules/node-firebird/lib/wire/connection.js → src/wire/connection.ts} +611 -129
- package/{poc/node_modules/node-firebird/lib/wire/const.js → src/wire/const.ts} +96 -9
- package/{poc/node_modules/node-firebird/lib/wire/database.js → src/wire/database.ts} +110 -44
- package/{poc/node_modules/node-firebird/lib/wire/eventConnection.js → src/wire/eventConnection.ts} +29 -14
- package/{poc/node_modules/node-firebird/lib/wire/fbEventManager.js → src/wire/fbEventManager.ts} +39 -30
- package/{poc/node_modules/node-firebird/lib/wire/serialize.js → src/wire/serialize.ts} +78 -59
- package/{poc/node_modules/node-firebird/lib/wire/service.js → src/wire/service.ts} +90 -88
- package/src/wire/socket.ts +262 -0
- package/src/wire/statement.ts +71 -0
- package/{poc/node_modules/node-firebird/lib/wire/transaction.js → src/wire/transaction.ts} +25 -19
- package/{poc/node_modules/node-firebird/lib/wire/xsqlvar.js → src/wire/xsqlvar.ts} +244 -143
- package/.eslintrc.json +0 -12
- package/.github/workflows/codeql.yml +0 -76
- package/.github/workflows/node.js.yml +0 -95
- package/BIGINT_MIGRATION.md +0 -374
- package/CI_DEBUGGING_GUIDE.md +0 -148
- package/ENCRYPTION_CALLBACK.md +0 -152
- package/FIREBIRD_LOG_FEATURE.md +0 -145
- package/MINIMAL_CHANGES_SUMMARY.md +0 -136
- package/PR_SUMMARY.md +0 -96
- package/ROADMAP.md +0 -223
- package/SRP_PROTOCOL.md +0 -482
- package/poc/README.md +0 -160
- package/poc/helpers.js +0 -59
- package/poc/node_modules/.package-lock.json +0 -14
- package/poc/node_modules/node-firebird/.eslintrc.json +0 -12
- package/poc/node_modules/node-firebird/.github/workflows/codeql.yml +0 -76
- package/poc/node_modules/node-firebird/.github/workflows/node.js.yml +0 -95
- package/poc/node_modules/node-firebird/BIGINT_MIGRATION.md +0 -374
- package/poc/node_modules/node-firebird/CI_DEBUGGING_GUIDE.md +0 -148
- package/poc/node_modules/node-firebird/ENCRYPTION_CALLBACK.md +0 -152
- package/poc/node_modules/node-firebird/FIREBIRD_LOG_FEATURE.md +0 -145
- package/poc/node_modules/node-firebird/LICENSE +0 -373
- package/poc/node_modules/node-firebird/MINIMAL_CHANGES_SUMMARY.md +0 -136
- package/poc/node_modules/node-firebird/PR_SUMMARY.md +0 -96
- package/poc/node_modules/node-firebird/README.md +0 -794
- package/poc/node_modules/node-firebird/ROADMAP.md +0 -223
- package/poc/node_modules/node-firebird/SRP_PROTOCOL.md +0 -482
- package/poc/node_modules/node-firebird/lib/callback.js +0 -38
- package/poc/node_modules/node-firebird/lib/gdscodes.d.ts +0 -1524
- package/poc/node_modules/node-firebird/lib/index.d.ts +0 -316
- package/poc/node_modules/node-firebird/lib/index.js +0 -128
- package/poc/node_modules/node-firebird/lib/pool.js +0 -108
- package/poc/node_modules/node-firebird/lib/wire/socket.js +0 -175
- package/poc/node_modules/node-firebird/lib/wire/statement.js +0 -48
- package/poc/node_modules/node-firebird/package.json +0 -38
- package/poc/node_modules/node-firebird/vitest.config.js +0 -24
- package/poc/package-lock.json +0 -21
- package/poc/package.json +0 -12
- package/poc/reproduce-fixed.js +0 -150
- package/poc/reproduce.js +0 -133
- package/vitest.config.js +0 -27
- /package/{poc/node_modules/node-firebird/lib → src}/firebird.msg +0 -0
- /package/{poc/node_modules/node-firebird/lib → src}/firebird.msg.json +0 -0
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
const net = require("net");
|
|
2
|
-
const zlib = require("zlib");
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Arc4 stream cipher for Firebird wire encryption.
|
|
6
|
-
* Uses the SRP session key to create RC4 encryption/decryption streams.
|
|
7
|
-
*/
|
|
8
|
-
class Arc4 {
|
|
9
|
-
constructor(key) {
|
|
10
|
-
this._s = new Uint8Array(256);
|
|
11
|
-
this._i = 0;
|
|
12
|
-
this._j = 0;
|
|
13
|
-
|
|
14
|
-
// KSA (Key-Scheduling Algorithm)
|
|
15
|
-
for (var i = 0; i < 256; i++) {
|
|
16
|
-
this._s[i] = i;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
var j = 0;
|
|
20
|
-
for (var i = 0; i < 256; i++) {
|
|
21
|
-
j = (j + this._s[i] + key[i % key.length]) & 0xFF;
|
|
22
|
-
// Swap
|
|
23
|
-
var tmp = this._s[i];
|
|
24
|
-
this._s[i] = this._s[j];
|
|
25
|
-
this._s[j] = tmp;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Transform (encrypt/decrypt) data in place.
|
|
31
|
-
* RC4 is symmetric - encrypt and decrypt are the same operation.
|
|
32
|
-
*/
|
|
33
|
-
transform(data) {
|
|
34
|
-
var out = Buffer.alloc(data.length);
|
|
35
|
-
for (var n = 0; n < data.length; n++) {
|
|
36
|
-
this._i = (this._i + 1) & 0xFF;
|
|
37
|
-
this._j = (this._j + this._s[this._i]) & 0xFF;
|
|
38
|
-
|
|
39
|
-
// Swap
|
|
40
|
-
var tmp = this._s[this._i];
|
|
41
|
-
this._s[this._i] = this._s[this._j];
|
|
42
|
-
this._s[this._j] = tmp;
|
|
43
|
-
|
|
44
|
-
var k = this._s[(this._s[this._i] + this._s[this._j]) & 0xFF];
|
|
45
|
-
out[n] = data[n] ^ k;
|
|
46
|
-
}
|
|
47
|
-
return out;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Socket proxy.
|
|
53
|
-
*/
|
|
54
|
-
class Socket {
|
|
55
|
-
constructor(port, host) {
|
|
56
|
-
this._socket = net.createConnection(port, host);
|
|
57
|
-
this._socket.setNoDelay(true);
|
|
58
|
-
this.compressor = null;
|
|
59
|
-
this.compressorBuffer = [];
|
|
60
|
-
this.decompressor = null;
|
|
61
|
-
this.decompressorBuffer = [];
|
|
62
|
-
this.buffer = null;
|
|
63
|
-
this.encrypt = false;
|
|
64
|
-
this.encryptCipher = null;
|
|
65
|
-
this.decryptCipher = null;
|
|
66
|
-
|
|
67
|
-
return new Proxy(this._socket, this);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Decompress and/or decrypt data when received.
|
|
72
|
-
* Override on data event.
|
|
73
|
-
*/
|
|
74
|
-
on(event, cb) {
|
|
75
|
-
if (event === 'data') {
|
|
76
|
-
const mainCb = cb;
|
|
77
|
-
cb = (data) => {
|
|
78
|
-
// Decrypt first if encryption is enabled
|
|
79
|
-
if (this.encrypt) {
|
|
80
|
-
data = this.decryptCipher.transform(data);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (this.compress) {
|
|
84
|
-
this.decompressor.write(data, () => {
|
|
85
|
-
mainCb(Buffer.concat(this.decompressorBuffer));
|
|
86
|
-
this.decompressorBuffer = []; // Reset buffer
|
|
87
|
-
});
|
|
88
|
-
} else {
|
|
89
|
-
mainCb(data);
|
|
90
|
-
}
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
this._socket.on(event, cb);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Compress and/or encrypt data before sending to socket.
|
|
99
|
-
*/
|
|
100
|
-
write(data, defer = false) {
|
|
101
|
-
if (defer) {
|
|
102
|
-
this.buffer = Buffer.from(data);
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
if (!defer && this.buffer) {
|
|
107
|
-
data = Buffer.concat([this.buffer, data]);
|
|
108
|
-
this.buffer = null;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (this.compress) {
|
|
112
|
-
this.compressor.write(data, () => {
|
|
113
|
-
var compressedData = Buffer.concat(this.compressorBuffer);
|
|
114
|
-
this.compressorBuffer = []; // Reset buffer
|
|
115
|
-
|
|
116
|
-
// Encrypt after compression if encryption is enabled
|
|
117
|
-
if (this.encrypt) {
|
|
118
|
-
compressedData = this.encryptCipher.transform(compressedData);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
this._socket.write(compressedData);
|
|
122
|
-
});
|
|
123
|
-
} else if (this.encrypt) {
|
|
124
|
-
this._socket.write(this.encryptCipher.transform(data));
|
|
125
|
-
} else {
|
|
126
|
-
this._socket.write(data);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Enable compression/decompression on the fly.
|
|
132
|
-
*/
|
|
133
|
-
enableCompression() {
|
|
134
|
-
this.compress = true;
|
|
135
|
-
|
|
136
|
-
// Create decompressor instance
|
|
137
|
-
this.decompressor = zlib.createInflate();
|
|
138
|
-
this.decompressor.on('data', (inflate) => {
|
|
139
|
-
this.decompressorBuffer.push(inflate);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
// Create compressor instance
|
|
143
|
-
this.compressor = zlib.createDeflate({
|
|
144
|
-
flush: zlib.constants.Z_FULL_FLUSH,
|
|
145
|
-
finishFlush: zlib.constants.Z_SYNC_FLUSH
|
|
146
|
-
});
|
|
147
|
-
this.compressor.on('data', (deflate) => {
|
|
148
|
-
this.compressorBuffer.push(deflate);
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Enable encryption/decryption using Arc4 cipher.
|
|
154
|
-
* @param {Buffer} sessionKey - The session key from SRP authentication.
|
|
155
|
-
*/
|
|
156
|
-
enableEncryption(sessionKey) {
|
|
157
|
-
this.encrypt = true;
|
|
158
|
-
this.encryptCipher = new Arc4(sessionKey);
|
|
159
|
-
this.decryptCipher = new Arc4(sessionKey);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Proxy trap.
|
|
164
|
-
*/
|
|
165
|
-
get(target, field) {
|
|
166
|
-
if (field in this) {
|
|
167
|
-
return this[field].bind(this);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
return target[field];
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
module.exports = Socket;
|
|
175
|
-
module.exports.Arc4 = Arc4;
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
/***************************************
|
|
2
|
-
*
|
|
3
|
-
* Statement
|
|
4
|
-
*
|
|
5
|
-
***************************************/
|
|
6
|
-
|
|
7
|
-
class Statement {
|
|
8
|
-
constructor(connection) {
|
|
9
|
-
this.connection = connection;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
close(callback) {
|
|
13
|
-
this.connection.closeStatement(this, callback);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
drop(callback) {
|
|
17
|
-
this.connection.dropStatement(this, callback);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
release(callback) {
|
|
21
|
-
var cache_query = this.connection.getCachedQuery(this.query);
|
|
22
|
-
if (cache_query)
|
|
23
|
-
this.connection.closeStatement(this, callback);
|
|
24
|
-
else
|
|
25
|
-
this.connection.dropStatement(this, callback);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
execute(transaction, params, callback, options) {
|
|
29
|
-
if (params instanceof Function) {
|
|
30
|
-
options = callback;
|
|
31
|
-
callback = params;
|
|
32
|
-
params = undefined;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
this.options = options;
|
|
36
|
-
this.connection.executeStatement(transaction, this, params, callback, options);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
fetch(transaction, count, callback) {
|
|
40
|
-
this.connection.fetch(this, transaction, count, callback);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
fetchAll(transaction, callback) {
|
|
44
|
-
this.connection.fetchAll(this, transaction, callback);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
module.exports = Statement;
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "node-firebird",
|
|
3
|
-
"version": "2.3.1",
|
|
4
|
-
"description": "Pure JavaScript and Asynchronous Firebird client for Node.js.",
|
|
5
|
-
"keywords": [
|
|
6
|
-
"firebird",
|
|
7
|
-
"database",
|
|
8
|
-
"rdbms",
|
|
9
|
-
"sql"
|
|
10
|
-
],
|
|
11
|
-
"homepage": "https://github.com/hgourvest/node-firebird",
|
|
12
|
-
"repository": {
|
|
13
|
-
"type": "git",
|
|
14
|
-
"url": "git+https://github.com/hgourvest/node-firebird.git"
|
|
15
|
-
},
|
|
16
|
-
"author": {
|
|
17
|
-
"name": "Henri Gourvest",
|
|
18
|
-
"email": "hgourvest@gmail.com"
|
|
19
|
-
},
|
|
20
|
-
"contributors": [
|
|
21
|
-
"Popa Marius Adrian <mapopa@gmail.com>",
|
|
22
|
-
"Peter Širka <petersirka@gmail.com>"
|
|
23
|
-
],
|
|
24
|
-
"main": "./lib",
|
|
25
|
-
"types": "./lib/index.d.ts",
|
|
26
|
-
"license": "MPL-2.0",
|
|
27
|
-
"scripts": {
|
|
28
|
-
"test": "vitest run",
|
|
29
|
-
"lint": "oxlint"
|
|
30
|
-
},
|
|
31
|
-
"dependencies": {
|
|
32
|
-
},
|
|
33
|
-
"devDependencies": {
|
|
34
|
-
"coveralls-next": "^6.0.1",
|
|
35
|
-
"oxlint": "^0.15.11",
|
|
36
|
-
"vitest": "^4.0.18"
|
|
37
|
-
}
|
|
38
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
const { defineConfig } = require('vitest/config');
|
|
2
|
-
|
|
3
|
-
module.exports = defineConfig({
|
|
4
|
-
test: {
|
|
5
|
-
globals: true,
|
|
6
|
-
testTimeout: 10000,
|
|
7
|
-
hookTimeout: 30000,
|
|
8
|
-
fileParallelism: false,
|
|
9
|
-
maxWorkers: 1,
|
|
10
|
-
isolate: false,
|
|
11
|
-
include: [
|
|
12
|
-
'test/arc4.js',
|
|
13
|
-
'test/protocol.js',
|
|
14
|
-
'test/srp.js',
|
|
15
|
-
'test/service.js',
|
|
16
|
-
'test/utf8-user-identification.js',
|
|
17
|
-
'test/index.js',
|
|
18
|
-
'test/db-crypt-config.js',
|
|
19
|
-
'test/timezone.js',
|
|
20
|
-
'test/decfloat.js',
|
|
21
|
-
'test/mock-server.js'
|
|
22
|
-
],
|
|
23
|
-
},
|
|
24
|
-
});
|
package/poc/package-lock.json
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "node-firebird-pool-poc",
|
|
3
|
-
"version": "1.0.0",
|
|
4
|
-
"lockfileVersion": 3,
|
|
5
|
-
"requires": true,
|
|
6
|
-
"packages": {
|
|
7
|
-
"": {
|
|
8
|
-
"name": "node-firebird-pool-poc",
|
|
9
|
-
"version": "1.0.0",
|
|
10
|
-
"dependencies": {
|
|
11
|
-
"node-firebird": "^2.3.1"
|
|
12
|
-
}
|
|
13
|
-
},
|
|
14
|
-
"node_modules/node-firebird": {
|
|
15
|
-
"version": "2.3.1",
|
|
16
|
-
"resolved": "https://registry.npmjs.org/node-firebird/-/node-firebird-2.3.1.tgz",
|
|
17
|
-
"integrity": "sha512-3yTwiLHwgtLYRv+aS4frzsQEwHMDXkP5Z1SPM/W103TvMemVRoCIaxPYO7axnUrKhgTfPHGXzqC5umYlQail/w==",
|
|
18
|
-
"license": "MPL-2.0"
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
}
|
package/poc/package.json
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "node-firebird-pool-poc",
|
|
3
|
-
"version": "1.0.0",
|
|
4
|
-
"description": "Proof of concept for node-firebird pool bugs: _creating stuck, pending leak, post-destroy get()",
|
|
5
|
-
"scripts": {
|
|
6
|
-
"bug": "node reproduce.js",
|
|
7
|
-
"fixed": "node reproduce-fixed.js"
|
|
8
|
-
},
|
|
9
|
-
"dependencies": {
|
|
10
|
-
"node-firebird": "^2.3.1"
|
|
11
|
-
}
|
|
12
|
-
}
|
package/poc/reproduce-fixed.js
DELETED
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* reproduce-fixed.js — Validates all fixes in pool-patched.js.
|
|
5
|
-
*
|
|
6
|
-
* Uses the same fake TCP server from reproduce.js (accepts connections but never
|
|
7
|
-
* responds to the Firebird wire protocol). The patched Pool is instantiated
|
|
8
|
-
* directly with Firebird's original attach function, so no server-side changes
|
|
9
|
-
* are needed and the fix is purely in lib/pool.js.
|
|
10
|
-
*
|
|
11
|
-
* ─── Fix 1 · connectTimeout in check() ──────────────────────────────────────
|
|
12
|
-
*
|
|
13
|
-
* A new options.connectTimeout value (ms) wraps the attach() call with a
|
|
14
|
-
* setTimeout. If attach() does not complete in time:
|
|
15
|
-
* • _creating is decremented (slot freed)
|
|
16
|
-
* • cb(Error) is called immediately (no hanging Promise)
|
|
17
|
-
* • setImmediate(check) is triggered (next pending request can be served)
|
|
18
|
-
* If attach() eventually returns a db after the timeout, the connection is
|
|
19
|
-
* discarded via db.connection._pooled = false + db.detach() (no socket leak).
|
|
20
|
-
*
|
|
21
|
-
* ─── Fix 2 · _destroyed flag in get() and check() ──────────────────────────
|
|
22
|
-
*
|
|
23
|
-
* pool.get() called after pool.destroy() is immediately rejected with
|
|
24
|
-
* "Pool has been destroyed". No silent accumulation in pending.
|
|
25
|
-
*
|
|
26
|
-
* ─── Fix 3 · _destroyed guard inside the attach() callback ─────────────────
|
|
27
|
-
*
|
|
28
|
-
* If pool.destroy() is called while an attach() is in flight, the late-
|
|
29
|
-
* arriving connection is discarded and the caller is notified with an error.
|
|
30
|
-
*
|
|
31
|
-
* ─── Fix 4 · destroy() drains pending ───────────────────────────────────────
|
|
32
|
-
*
|
|
33
|
-
* All callbacks queued in this.pending when destroy() is called receive an
|
|
34
|
-
* immediate "Pool is being destroyed" error instead of hanging forever.
|
|
35
|
-
*
|
|
36
|
-
* ─── Fix 5 · destroy() counts in-use connections ────────────────────────────
|
|
37
|
-
*
|
|
38
|
-
* Connections currently in use (dbinuse) are now counted down so that the
|
|
39
|
-
* destroy() completion callback is not blocked when in-use connections exist.
|
|
40
|
-
*
|
|
41
|
-
* ─── How to run ──────────────────────────────────────────────────────────────
|
|
42
|
-
*
|
|
43
|
-
* npm install
|
|
44
|
-
* node reproduce-fixed.js
|
|
45
|
-
*
|
|
46
|
-
* Expected: all callbacks receive proper errors, _creating returns to 0,
|
|
47
|
-
* process exits cleanly with code 0.
|
|
48
|
-
*/
|
|
49
|
-
|
|
50
|
-
const Firebird = require('node-firebird');
|
|
51
|
-
const PatchedPool = require('../lib/pool');
|
|
52
|
-
const { log, poolState, startFakeServer } = require('./helpers');
|
|
53
|
-
|
|
54
|
-
const FAKE_PORT = 13051; // different port from reproduce.js to avoid conflicts
|
|
55
|
-
const CONNECT_TIMEOUT = 1500; // 1.5 s — short for demo; use 5–10 s in production
|
|
56
|
-
const TICK_MS = 600;
|
|
57
|
-
|
|
58
|
-
startFakeServer(FAKE_PORT, (server) => {
|
|
59
|
-
|
|
60
|
-
const options = {
|
|
61
|
-
host: '127.0.0.1',
|
|
62
|
-
port: FAKE_PORT,
|
|
63
|
-
database: '/tmp/poc.fdb',
|
|
64
|
-
user: 'SYSDBA',
|
|
65
|
-
password: 'masterkey',
|
|
66
|
-
lowercase_keys: true,
|
|
67
|
-
isPool: true, // mirrors what exports.pool() adds internally
|
|
68
|
-
connectTimeout: CONNECT_TIMEOUT, // ← the new option (Fix 1)
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
// Instantiate the patched Pool directly with Firebird's original attach function.
|
|
72
|
-
// This is equivalent to what exports.pool() does internally:
|
|
73
|
-
// return new Pool(exports.attach, max, Object.assign({}, options, { isPool: true }));
|
|
74
|
-
const pool = new PatchedPool(Firebird.attach, 5, options);
|
|
75
|
-
|
|
76
|
-
console.log('\n══════════════════════════════════════════════════════════════════');
|
|
77
|
-
console.log(' node-firebird · pool fix validation (pool-patched.js)');
|
|
78
|
-
console.log(` connectTimeout = ${CONNECT_TIMEOUT} ms`);
|
|
79
|
-
console.log('══════════════════════════════════════════════════════════════════\n');
|
|
80
|
-
|
|
81
|
-
// ── Fix 1 ─────────────────────────────────────────────────────────────────
|
|
82
|
-
log('FIX-1', `pool.get() — attach() will hang, timeout fires after ${CONNECT_TIMEOUT} ms`);
|
|
83
|
-
|
|
84
|
-
pool.get((err, db) => {
|
|
85
|
-
if (err) log('FIX-1 ✓ OK ', `callback received error (expected): "${err.message}"`);
|
|
86
|
-
else log('FIX-1 ✗ WRONG ', `got db — should have timed out`);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
let tick = 0;
|
|
90
|
-
|
|
91
|
-
const interval = setInterval(() => {
|
|
92
|
-
tick++;
|
|
93
|
-
log('pool-state', poolState(pool));
|
|
94
|
-
|
|
95
|
-
// ── Fix 2 + Fix 4 (setup) ─────────────────────────────────────────────
|
|
96
|
-
if (tick === 3) {
|
|
97
|
-
// At this point the connectTimeout for tick-1 has already fired
|
|
98
|
-
// (1500 ms < 3 × 600 ms = 1800 ms), so pool is idle again.
|
|
99
|
-
log('FIX-4', 'Queuing a get() in pending, then destroying pool immediately');
|
|
100
|
-
|
|
101
|
-
pool.get((err, db) => {
|
|
102
|
-
// [Fix 4] destroy() must drain this callback right away.
|
|
103
|
-
if (err) log('FIX-4 ✓ OK ', `pending callback received error: "${err.message}"`);
|
|
104
|
-
else log('FIX-4 ✗ WRONG ', `got db after destroy: ${!!db}`);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
log('FIX-4', `Before destroy — ${poolState(pool)}`);
|
|
108
|
-
|
|
109
|
-
// Suspend check() by inflating _creating so our queued get() stays in pending.
|
|
110
|
-
// (In real usage a second concurrent pool.get() while one is stuck in attach()
|
|
111
|
-
// would naturally be in pending — this simulates that scenario without timing
|
|
112
|
-
// tricks that make the PoC fragile.)
|
|
113
|
-
pool._creating = 5; // fill all slots so check() won't serve the pending cb
|
|
114
|
-
pool.destroy();
|
|
115
|
-
pool._creating = 0; // reset (already orphaned pool, just for readable output)
|
|
116
|
-
|
|
117
|
-
log('FIX-4', `After destroy — ${poolState(pool)}`);
|
|
118
|
-
log('FIX-4', '↑ pending callback fired SYNCHRONOUSLY inside destroy()');
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// ── Fix 2 ──────────────────────────────────────────────────────────────
|
|
122
|
-
if (tick === 4) {
|
|
123
|
-
log('FIX-2', 'pool.get() AFTER pool.destroy() — must be rejected immediately');
|
|
124
|
-
|
|
125
|
-
pool.get((err, db) => {
|
|
126
|
-
// [Fix 2] _destroyed guard in get() rejects this synchronously.
|
|
127
|
-
if (err) log('FIX-2 ✓ OK ', `immediately rejected: "${err.message}"`);
|
|
128
|
-
else log('FIX-2 ✗ WRONG ', `got db after destroy: ${!!db}`);
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
log('FIX-2', `After post-destroy get — ${poolState(pool)}`);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// ── Summary ────────────────────────────────────────────────────────────
|
|
135
|
-
if (tick === 5) {
|
|
136
|
-
clearInterval(interval);
|
|
137
|
-
|
|
138
|
-
console.log('\n══════════════════════════════════════════════════════════════════');
|
|
139
|
-
log('SUMMARY', `Final state : ${poolState(pool)}`);
|
|
140
|
-
log('SUMMARY', `Fix 1 confirmed : _creating returned to 0 after ${CONNECT_TIMEOUT} ms timeout`);
|
|
141
|
-
log('SUMMARY', 'Fix 2 confirmed : post-destroy get() rejected immediately (_destroyed flag)');
|
|
142
|
-
log('SUMMARY', 'Fix 4 confirmed : pending callback drained synchronously by destroy()');
|
|
143
|
-
log('SUMMARY', 'Process exits cleanly — no hanging sockets or unresolved callbacks');
|
|
144
|
-
console.log('══════════════════════════════════════════════════════════════════\n');
|
|
145
|
-
|
|
146
|
-
server.close();
|
|
147
|
-
process.exit(0); // exit(0) = all fixes validated
|
|
148
|
-
}
|
|
149
|
-
}, TICK_MS);
|
|
150
|
-
});
|
package/poc/reproduce.js
DELETED
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* reproduce.js — Demonstrates three bugs in node-firebird's Pool class (lib/pool.js).
|
|
5
|
-
*
|
|
6
|
-
* No real Firebird server is required. A fake TCP server (see helpers.js) accepts
|
|
7
|
-
* connections but never responds to the Firebird wire protocol, triggering the bugs.
|
|
8
|
-
*
|
|
9
|
-
* ─── Bug 1 · _creating stuck forever ────────────────────────────────────────
|
|
10
|
-
*
|
|
11
|
-
* In Pool.check(), when no idle connection exists:
|
|
12
|
-
*
|
|
13
|
-
* self._creating++;
|
|
14
|
-
* this.attach(options, function(err, db) {
|
|
15
|
-
* self._creating--; // ← only here
|
|
16
|
-
* ...
|
|
17
|
-
* cb(err, db);
|
|
18
|
-
* });
|
|
19
|
-
*
|
|
20
|
-
* If attach() never calls back (TCP connects, but server stalls on Firebird
|
|
21
|
-
* protocol, e.g. SRP auth), _creating is never decremented. The pool slot is
|
|
22
|
-
* permanently locked. pool.get() callers wait forever.
|
|
23
|
-
*
|
|
24
|
-
* ─── Bug 2 · destroy() does not drain pending callbacks ─────────────────────
|
|
25
|
-
*
|
|
26
|
-
* pool.destroy() only iterates this.internaldb (connected databases). Callbacks
|
|
27
|
-
* queued in this.pending (waiting for a free slot) are silently abandoned — they
|
|
28
|
-
* are never called, so any awaiting Promise hangs until the process exits.
|
|
29
|
-
*
|
|
30
|
-
* Additionally, if any connection is currently in use (dbinuse > 0), the
|
|
31
|
-
* destroy() callback itself is never invoked because connectionCount never
|
|
32
|
-
* reached zero (the in-use branch falls through without calling detachCallback).
|
|
33
|
-
*
|
|
34
|
-
* ─── Bug 3 · pool.get() after pool.destroy() silently accumulates ────────────
|
|
35
|
-
*
|
|
36
|
-
* There is no _destroyed guard. Calling pool.get() on a destroyed pool pushes
|
|
37
|
-
* the callback into this.pending and calls check(), which returns early because
|
|
38
|
-
* _destroyed is never checked. The callback is never served.
|
|
39
|
-
*
|
|
40
|
-
* ─── How to run ──────────────────────────────────────────────────────────────
|
|
41
|
-
*
|
|
42
|
-
* npm install
|
|
43
|
-
* node reproduce.js
|
|
44
|
-
*
|
|
45
|
-
* Expected: process does NOT exit cleanly (hangs due to open sockets / pending
|
|
46
|
-
* callbacks). Exit code 1 is forced after the demo to confirm bugs are present.
|
|
47
|
-
*/
|
|
48
|
-
|
|
49
|
-
const Firebird = require('node-firebird');
|
|
50
|
-
const { log, poolState, startFakeServer } = require('./helpers');
|
|
51
|
-
|
|
52
|
-
const FAKE_PORT = 13050;
|
|
53
|
-
const TICK_MS = 600; // interval between pool-state snapshots
|
|
54
|
-
|
|
55
|
-
startFakeServer(FAKE_PORT, (server) => {
|
|
56
|
-
|
|
57
|
-
const options = {
|
|
58
|
-
host: '127.0.0.1',
|
|
59
|
-
port: FAKE_PORT,
|
|
60
|
-
database: '/tmp/poc.fdb',
|
|
61
|
-
user: 'SYSDBA',
|
|
62
|
-
password: 'masterkey',
|
|
63
|
-
lowercase_keys: true,
|
|
64
|
-
isPool: true,
|
|
65
|
-
// No connectTimeout — original (buggy) behaviour
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
const pool = Firebird.pool(5, options);
|
|
69
|
-
|
|
70
|
-
console.log('\n══════════════════════════════════════════════════════════════════');
|
|
71
|
-
console.log(' node-firebird · pool bug reproduction (original lib/pool.js)');
|
|
72
|
-
console.log('══════════════════════════════════════════════════════════════════\n');
|
|
73
|
-
|
|
74
|
-
// ── Bug 1 ─────────────────────────────────────────────────────────────────
|
|
75
|
-
log('BUG-1', 'pool.get() → TCP connects, Firebird protocol hangs, callback NEVER fires');
|
|
76
|
-
|
|
77
|
-
pool.get((err, db) => {
|
|
78
|
-
// ← BUG: this line is never reached
|
|
79
|
-
log('BUG-1 ✗ WRONG', `callback fired (unexpected): err=${err?.message} db=${!!db}`);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
let tick = 0;
|
|
83
|
-
|
|
84
|
-
const interval = setInterval(() => {
|
|
85
|
-
tick++;
|
|
86
|
-
log('pool-state', poolState(pool));
|
|
87
|
-
|
|
88
|
-
// ── Bug 2 (setup) ──────────────────────────────────────────────────────
|
|
89
|
-
if (tick === 2) {
|
|
90
|
-
log('BUG-2', 'Queuing a second get() in pending, then calling destroy()');
|
|
91
|
-
|
|
92
|
-
pool.get((err, db) => {
|
|
93
|
-
// ← BUG: destroy() never calls this; the Promise hangs forever
|
|
94
|
-
if (err) log('BUG-2 ✓ OK ', `pending callback received error: "${err.message}"`);
|
|
95
|
-
else log('BUG-2 ✗ WRONG ', `got db unexpectedly: ${!!db}`);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
log('BUG-2', `Before destroy — ${poolState(pool)}`);
|
|
99
|
-
pool.destroy();
|
|
100
|
-
log('BUG-2', `After destroy — ${poolState(pool)}`);
|
|
101
|
-
log('BUG-2', '↑ pending callback was NEVER called — Promise hangs forever');
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// ── Bug 3 ──────────────────────────────────────────────────────────────
|
|
105
|
-
if (tick === 3) {
|
|
106
|
-
log('BUG-3', 'pool.get() AFTER pool.destroy() — should be rejected immediately');
|
|
107
|
-
|
|
108
|
-
pool.get((err, db) => {
|
|
109
|
-
// ← BUG: no _destroyed guard; callback silently queued, never served
|
|
110
|
-
if (err) log('BUG-3 ✓ OK ', `rejected: "${err.message}"`);
|
|
111
|
-
else log('BUG-3 ✗ WRONG ', `got db after destroy: ${!!db}`);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
log('BUG-3', `After post-destroy get — ${poolState(pool)}`);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// ── Summary ────────────────────────────────────────────────────────────
|
|
118
|
-
if (tick === 5) {
|
|
119
|
-
clearInterval(interval);
|
|
120
|
-
|
|
121
|
-
console.log('\n══════════════════════════════════════════════════════════════════');
|
|
122
|
-
log('SUMMARY', `Final state : ${poolState(pool)}`);
|
|
123
|
-
log('SUMMARY', 'Bug 1 confirmed : _creating=1, permanently stuck (slot lost)');
|
|
124
|
-
log('SUMMARY', 'Bug 2 confirmed : pending callback from before destroy never fired');
|
|
125
|
-
log('SUMMARY', 'Bug 3 confirmed : post-destroy get silently accumulated in pending');
|
|
126
|
-
log('SUMMARY', 'Process would hang forever without forced exit below');
|
|
127
|
-
console.log('══════════════════════════════════════════════════════════════════\n');
|
|
128
|
-
|
|
129
|
-
server.close();
|
|
130
|
-
process.exit(1); // exit(1) = bugs confirmed
|
|
131
|
-
}
|
|
132
|
-
}, TICK_MS);
|
|
133
|
-
});
|
package/vitest.config.js
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
const { defineConfig } = require('vitest/config');
|
|
2
|
-
|
|
3
|
-
module.exports = defineConfig({
|
|
4
|
-
test: {
|
|
5
|
-
globals: true,
|
|
6
|
-
testTimeout: 10000,
|
|
7
|
-
hookTimeout: 30000,
|
|
8
|
-
fileParallelism: false,
|
|
9
|
-
maxWorkers: 1,
|
|
10
|
-
isolate: false,
|
|
11
|
-
include: [
|
|
12
|
-
'test/arc4.js',
|
|
13
|
-
'test/protocol.js',
|
|
14
|
-
'test/srp.js',
|
|
15
|
-
'test/service.js',
|
|
16
|
-
'test/utf8-user-identification.js',
|
|
17
|
-
'test/index.js',
|
|
18
|
-
'test/db-crypt-config.js',
|
|
19
|
-
'test/encoding.js',
|
|
20
|
-
'test/timezone.js',
|
|
21
|
-
'test/decfloat.js',
|
|
22
|
-
'test/mock-server.js',
|
|
23
|
-
'test/pool-fixes.js',
|
|
24
|
-
'test/blob-chunks.js'
|
|
25
|
-
],
|
|
26
|
-
},
|
|
27
|
-
});
|
|
File without changes
|
|
File without changes
|