web 기본셋팅

This commit is contained in:
2026-06-22 22:17:24 +09:00
parent a195e81dc0
commit 5e61a5de18
2081 changed files with 857581 additions and 2292 deletions
+19
View File
@@ -0,0 +1,19 @@
Copyright (c) 2016 Andrey Sidorov (sidorares@yandex.ru) and contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+114
View File
@@ -0,0 +1,114 @@
[npm-image]: https://img.shields.io/npm/v/mysql2.svg
[npm-url]: https://npmjs.com/package/mysql2
[node-version-image]: https://img.shields.io/node/v/mysql2.svg
[node-version-url]: https://nodejs.org/en/download
[downloads-image]: https://img.shields.io/npm/dm/mysql2.svg
[downloads-url]: https://npmjs.com/package/mysql2
[license-url]: https://github.com/sidorares/node-mysql2/blob/master/License
[license-image]: https://img.shields.io/npm/l/mysql2.svg?maxAge=2592000
[node-mysql]: https://github.com/mysqljs/mysql
[mysqljs]: https://github.com/mysqljs
[mysql-native]: https://github.com/sidorares/nodejs-mysql-native
[sidorares]: https://github.com/sidorares
[TooTallNate]: https://gist.github.com/TooTallNate
[starttls.js]: https://gist.github.com/TooTallNate/848444
[node-mariasql]: https://github.com/mscdex/node-mariasql
[contributors]: https://github.com/sidorares/node-mysql2/graphs/contributors
[contributing]: https://github.com/sidorares/node-mysql2/blob/master/Contributing.md
[docs-base]: https://sidorares.github.io/node-mysql2/docs
[docs-base-zh-CN]: https://sidorares.github.io/node-mysql2/zh-CN/docs
[docs-base-pt-BR]: https://sidorares.github.io/node-mysql2/pt-BR/docs
[docs-prepared-statements]: https://sidorares.github.io/node-mysql2/docs/documentation/prepared-statements
[docs-mysql-server]: https://sidorares.github.io/node-mysql2/docs/documentation/mysql-server
[docs-promise-wrapper]: https://sidorares.github.io/node-mysql2/docs/documentation/promise-wrapper
[docs-authentication-switch]: https://sidorares.github.io/node-mysql2/docs/documentation/authentication-switch
[docs-streams]: https://sidorares.github.io/node-mysql2/docs/documentation/extras
[docs-typescript-docs]: https://sidorares.github.io/node-mysql2/docs/documentation/typescript-examples
[docs-qs-pooling]: https://sidorares.github.io/node-mysql2/docs#using-connection-pools
[docs-qs-first-query]: https://sidorares.github.io/node-mysql2/docs#first-query
[docs-qs-using-prepared-statements]: https://sidorares.github.io/node-mysql2/docs#using-prepared-statements
[docs-examples]: https://sidorares.github.io/node-mysql2/docs/examples
[docs-faq]: https://sidorares.github.io/node-mysql2/docs/faq
[docs-documentation]: https://sidorares.github.io/node-mysql2/docs/documentation
[docs-contributing]: https://sidorares.github.io/node-mysql2/docs/contributing/website
[coverage]: https://img.shields.io/codecov/c/github/sidorares/node-mysql2
[coverage-url]: https://app.codecov.io/github/sidorares/node-mysql2
[ci-url]: https://github.com/sidorares/node-mysql2/actions/workflows/ci-coverage.yml?query=branch%3Amaster
[ci-image]: https://img.shields.io/github/actions/workflow/status/sidorares/node-mysql2/ci-coverage.yml?event=push&style=flat&label=CI&branch=master
# MySQL2
[![NPM Version][npm-image]][npm-url]
[![NPM Downloads][downloads-image]][downloads-url]
[![Node.js Version][node-version-image]][node-version-url]
[![GitHub Workflow Status (with event)][ci-image]][ci-url]
[![Codecov][coverage]][coverage-url]
[![License][license-image]][license-url]
[English][docs-base] | [简体中文][docs-base-zh-CN] | [Português (BR)][docs-base-pt-BR]
> MySQL client for Node.js with focus on performance. Supports prepared statements, non-utf8 encodings, binary log protocol, compression, ssl [much more][docs-documentation].
**Table of Contents**
- [History and Why MySQL2](#history-and-why-mysql2)
- [Installation](#installation)
- [Documentation](#documentation)
- [Acknowledgements](#acknowledgements)
- [Contributing](#contributing)
## History and Why MySQL2
MySQL2 project is a continuation of [MySQL-Native][mysql-native]. Protocol parser code was rewritten from scratch and api changed to match popular [Node MySQL][node-mysql]. MySQL2 team is working together with [Node MySQL][node-mysql] team to factor out shared code and move it under [mysqljs][mysqljs] organization.
MySQL2 is mostly API compatible with [Node MySQL][node-mysql] and supports majority of features. MySQL2 also offers these additional features:
- Faster / Better Performance
- [Prepared Statements][docs-prepared-statements]
- MySQL Binary Log Protocol
- [MySQL Server][docs-mysql-server]
- Extended support for Encoding and Collation
- [Promise Wrapper][docs-promise-wrapper]
- Compression
- SSL and [Authentication Switch][docs-authentication-switch]
- [Custom Streams][docs-streams]
- [Pooling][docs-qs-pooling]
## Installation
MySQL2 is free from native bindings and can be installed on Linux, Mac OS or Windows without any issues.
```bash
npm install --save mysql2
```
If you are using TypeScript, you will need to install `@types/node`.
```bash
npm install --save-dev @types/node
```
> For TypeScript documentation and examples, see [here][docs-typescript-docs].
## Documentation
- [Quickstart][docs-base]
- [First Query][docs-qs-first-query], [Using Prepared Statements][docs-qs-using-prepared-statements], [Using Connection Pools][docs-qs-pooling] and more.
- [Documentation][docs-documentation]
- [Examples][docs-examples]
- [FAQ][docs-faq]
## Acknowledgements
- Internal protocol is written by [@sidorares][sidorares] [MySQL-Native][mysql-native].
- Constants, SQL parameters interpolation, Pooling, `ConnectionConfig` class taken from [Node MySQL][node-mysql].
- SSL upgrade code based on [@TooTallNate][TooTallNate] [code][starttls.js].
- Secure connection / compressed connection api flags compatible to [MariaSQL][node-mariasql] client.
- [Contributors][contributors].
## Contributing
Want to improve something in **MySQL2**?
Please check [Contributing.md][contributing] for detailed instruction on how to get started.
To contribute in **MySQL2 Documentation**, please visit the [Website Contributing Guidelines][docs-contributing] for detailed instruction on how to get started.
+1
View File
@@ -0,0 +1 @@
export * from './typings/mysql/index.js';
+77
View File
@@ -0,0 +1,77 @@
'use strict';
const SqlString = require('sql-escaper');
const ConnectionConfig = require('./lib/connection_config.js');
const parserCache = require('./lib/parsers/parser_cache.js');
const Connection = require('./lib/connection.js');
exports.createConnection = require('./lib/create_connection.js');
exports.connect = exports.createConnection;
exports.Connection = Connection;
exports.ConnectionConfig = ConnectionConfig;
const Pool = require('./lib/pool.js');
const PoolCluster = require('./lib/pool_cluster.js');
const createPool = require('./lib/create_pool.js');
const createPoolCluster = require('./lib/create_pool_cluster.js');
exports.createPool = createPool;
exports.createPoolCluster = createPoolCluster;
exports.createQuery = Connection.createQuery;
exports.Pool = Pool;
exports.PoolCluster = PoolCluster;
exports.createServer = function (handler) {
const Server = require('./lib/server.js');
const s = new Server();
if (handler) {
s.on('connection', handler);
}
return s;
};
exports.PoolConnection = require('./lib/pool_connection.js');
exports.authPlugins = require('./lib/auth_plugins');
exports.escape = SqlString.escape;
exports.escapeId = SqlString.escapeId;
exports.format = SqlString.format;
exports.raw = SqlString.raw;
exports.__defineGetter__(
'createConnectionPromise',
() => require('./promise.js').createConnection
);
exports.__defineGetter__(
'createPoolPromise',
() => require('./promise.js').createPool
);
exports.__defineGetter__(
'createPoolClusterPromise',
() => require('./promise.js').createPoolCluster
);
exports.__defineGetter__('Types', () => require('./lib/constants/types.js'));
exports.__defineGetter__('Charsets', () =>
require('./lib/constants/charsets.js')
);
exports.__defineGetter__('CharsetToEncoding', () =>
require('./lib/constants/charset_encodings.js')
);
exports.setMaxParserCache = function (max) {
parserCache.setMaxCache(max);
};
exports.clearParserCache = function () {
parserCache.clearCache();
};
+95
View File
@@ -0,0 +1,95 @@
'use strict';
/*
4.1 authentication: (http://bazaar.launchpad.net/~mysql/mysql-server/5.5/view/head:/sql/password.c)
SERVER: public_seed=create_random_string()
send(public_seed)
CLIENT: recv(public_seed)
hash_stage1=sha1("password")
hash_stage2=sha1(hash_stage1)
reply=xor(hash_stage1, sha1(public_seed,hash_stage2)
// this three steps are done in scramble()
send(reply)
SERVER: recv(reply)
hash_stage1=xor(reply, sha1(public_seed,hash_stage2))
candidate_hash2=sha1(hash_stage1)
check(candidate_hash2==hash_stage2)
server stores sha1(sha1(password)) ( hash_stag2)
*/
const crypto = require('crypto');
function sha1(msg, msg1, msg2) {
const hash = crypto.createHash('sha1');
hash.update(msg);
if (msg1) {
hash.update(msg1);
}
if (msg2) {
hash.update(msg2);
}
return hash.digest();
}
function xor(a, b) {
const result = Buffer.allocUnsafe(a.length);
for (let i = 0; i < a.length; i++) {
result[i] = a[i] ^ b[i];
}
return result;
}
exports.xor = xor;
function token(password, scramble1, scramble2) {
if (!password) {
return Buffer.alloc(0);
}
const stage1 = sha1(password);
return exports.calculateTokenFromPasswordSha(stage1, scramble1, scramble2);
}
exports.calculateTokenFromPasswordSha = function (
passwordSha,
scramble1,
scramble2
) {
// we use AUTH 41 here, and we need only the bytes we just need.
const authPluginData1 = scramble1.slice(0, 8);
const authPluginData2 = scramble2.slice(0, 12);
const stage2 = sha1(passwordSha);
const stage3 = sha1(authPluginData1, authPluginData2, stage2);
return xor(stage3, passwordSha);
};
exports.calculateToken = token;
exports.verifyToken = function (publicSeed1, publicSeed2, token, doubleSha) {
const hashStage1 = xor(token, sha1(publicSeed1, publicSeed2, doubleSha));
const candidateHash2 = sha1(hashStage1);
return candidateHash2.compare(doubleSha) === 0;
};
exports.doubleSha1 = function (password) {
return sha1(sha1(password));
};
function xorRotating(a, seed) {
const result = Buffer.allocUnsafe(a.length);
const seedLen = seed.length;
for (let i = 0; i < a.length; i++) {
result[i] = a[i] ^ seed[i % seedLen];
}
return result;
}
exports.xorRotating = xorRotating;
+115
View File
@@ -0,0 +1,115 @@
'use strict';
// https://mysqlserverteam.com/mysql-8-0-4-new-default-authentication-plugin-caching_sha2_password/
const PLUGIN_NAME = 'caching_sha2_password';
const crypto = require('crypto');
const { xor, xorRotating } = require('../auth_41');
const REQUEST_SERVER_KEY_PACKET = Buffer.from([2]);
const FAST_AUTH_SUCCESS_PACKET = Buffer.from([3]);
const PERFORM_FULL_AUTHENTICATION_PACKET = Buffer.from([4]);
const STATE_INITIAL = 0;
const STATE_TOKEN_SENT = 1;
const STATE_WAIT_SERVER_KEY = 2;
const STATE_FINAL = -1;
function sha256(msg) {
const hash = crypto.createHash('sha256');
hash.update(msg);
return hash.digest();
}
function calculateToken(password, scramble) {
if (!password) {
return Buffer.alloc(0);
}
const stage1 = sha256(Buffer.from(password));
const stage2 = sha256(stage1);
const stage3 = sha256(Buffer.concat([stage2, scramble]));
return xor(stage1, stage3);
}
function encrypt(password, scramble, key) {
const stage1 = xorRotating(Buffer.from(`${password}\0`, 'utf8'), scramble);
return crypto.publicEncrypt(
{
key,
oaepHash: 'sha1',
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
},
stage1
);
}
const pluginFactory =
(pluginOptions = {}) =>
({ connection }) => {
let state = 0;
let scramble = null;
const password = connection.config.password;
const authWithKey = (serverKey) => {
const _password = encrypt(password, scramble, serverKey);
state = STATE_FINAL;
return _password;
};
return (data) => {
switch (state) {
case STATE_INITIAL:
scramble = data.slice(0, 20);
state = STATE_TOKEN_SENT;
return calculateToken(password, scramble);
case STATE_TOKEN_SENT:
if (FAST_AUTH_SUCCESS_PACKET.equals(data)) {
state = STATE_FINAL;
return null;
}
if (PERFORM_FULL_AUTHENTICATION_PACKET.equals(data)) {
const isSecureConnection =
typeof pluginOptions.overrideIsSecure === 'undefined'
? connection.config.ssl || connection.config.socketPath
: pluginOptions.overrideIsSecure;
if (isSecureConnection) {
state = STATE_FINAL;
return Buffer.from(`${password}\0`, 'utf8');
}
// if client provides key we can save one extra roundrip on first connection
if (pluginOptions.serverPublicKey) {
return authWithKey(pluginOptions.serverPublicKey);
}
state = STATE_WAIT_SERVER_KEY;
return REQUEST_SERVER_KEY_PACKET;
}
throw new Error(
`Invalid AuthMoreData packet received by ${PLUGIN_NAME} plugin in STATE_TOKEN_SENT state.`
);
case STATE_WAIT_SERVER_KEY:
if (pluginOptions.onServerPublicKey) {
pluginOptions.onServerPublicKey(data);
}
return authWithKey(data);
case STATE_FINAL:
throw new Error(
`Unexpected data in AuthMoreData packet received by ${PLUGIN_NAME} plugin in STATE_FINAL state.`
);
}
throw new Error(
`Unexpected data in AuthMoreData packet received by ${PLUGIN_NAME} plugin in state ${state}`
);
};
};
// Export the plugin factory as default
module.exports = pluginFactory;
// Export calculateToken for reuse in initial handshake optimization
module.exports.calculateToken = calculateToken;
+18
View File
@@ -0,0 +1,18 @@
##
https://dev.mysql.com/doc/refman/8.0/en/caching-sha2-pluggable-authentication.html
```js
const mysql = require('mysql');
mysql.createConnection({
authPlugins: {
caching_sha2_password: mysql.authPlugins.caching_sha2_password({
onServerPublikKey: function (key) {
console.log(key);
},
serverPublicKey: 'xxxyyy',
overrideIsSecure: true, //
}),
},
});
```
+8
View File
@@ -0,0 +1,8 @@
'use strict';
module.exports = {
caching_sha2_password: require('./caching_sha2_password'),
mysql_clear_password: require('./mysql_clear_password'),
mysql_native_password: require('./mysql_native_password'),
sha256_password: require('./sha256_password'),
};
+17
View File
@@ -0,0 +1,17 @@
'use strict';
function bufferFromStr(str) {
return Buffer.from(`${str}\0`);
}
const create_mysql_clear_password_plugin = (pluginOptions) =>
function mysql_clear_password_plugin({ connection, command }) {
const password =
command.password || pluginOptions.password || connection.config.password;
return function (/* pluginData */) {
return bufferFromStr(password);
};
};
module.exports = create_mysql_clear_password_plugin;
+34
View File
@@ -0,0 +1,34 @@
'use strict';
//const PLUGIN_NAME = 'mysql_native_password';
const auth41 = require('../auth_41.js');
module.exports =
(pluginOptions) =>
({ connection, command }) => {
const password =
command.password || pluginOptions.password || connection.config.password;
const passwordSha1 =
command.passwordSha1 ||
pluginOptions.passwordSha1 ||
connection.config.passwordSha1;
return (data) => {
const authPluginData1 = data.slice(0, 8);
const authPluginData2 = data.slice(8, 20);
let authToken;
if (passwordSha1) {
authToken = auth41.calculateTokenFromPasswordSha(
passwordSha1,
authPluginData1,
authPluginData2
);
} else {
authToken = auth41.calculateToken(
password,
authPluginData1,
authPluginData2
);
}
return authToken;
};
};
+74
View File
@@ -0,0 +1,74 @@
'use strict';
const PLUGIN_NAME = 'sha256_password';
const crypto = require('crypto');
const { xorRotating } = require('../auth_41');
const Tls = require('tls');
const REQUEST_SERVER_KEY_PACKET = Buffer.from([1]);
const STATE_INITIAL = 0;
const STATE_WAIT_SERVER_KEY = 1;
const STATE_FINAL = -1;
function encrypt(password, scramble, key) {
const stage1 = xorRotating(Buffer.from(`${password}\0`, 'utf8'), scramble);
return crypto.publicEncrypt(
{
key,
oaepHash: 'sha1',
},
stage1
);
}
module.exports =
(pluginOptions = {}) =>
({ connection }) => {
let state = 0;
let scramble = null;
const password = connection.config.password;
const authWithKey = (serverKey) => {
const _password = encrypt(password, scramble, serverKey);
state = STATE_FINAL;
return _password;
};
return (data) => {
switch (state) {
case STATE_INITIAL:
if (
connection.stream instanceof Tls.TLSSocket &&
connection.stream.encrypted === true
) {
// We don't need to encrypt passwords over TLS connection
return Buffer.from(`${password}\0`, 'utf8');
}
scramble = data.slice(0, 20);
// if client provides key we can save one extra roundrip on first connection
if (pluginOptions.serverPublicKey) {
return authWithKey(pluginOptions.serverPublicKey);
}
state = STATE_WAIT_SERVER_KEY;
return REQUEST_SERVER_KEY_PACKET;
case STATE_WAIT_SERVER_KEY:
if (pluginOptions.onServerPublicKey) {
pluginOptions.onServerPublicKey(data);
}
return authWithKey(data);
case STATE_FINAL:
throw new Error(
`Unexpected data in AuthMoreData packet received by ${PLUGIN_NAME} plugin in STATE_FINAL state.`
);
}
throw new Error(
`Unexpected data in AuthMoreData packet received by ${PLUGIN_NAME} plugin in state ${state}`
);
};
};
File diff suppressed because it is too large Load Diff
+347
View File
@@ -0,0 +1,347 @@
'use strict';
const process = require('process');
const SqlString = require('sql-escaper');
const EventEmitter = require('events').EventEmitter;
const PoolConnection = require('../pool_connection.js');
const Queue = require('denque');
const BaseConnection = require('./connection.js');
const Errors = require('../constants/errors.js');
const {
traceCallback,
getServerContext,
poolConnectChannel,
} = require('../tracing.js');
// Source: https://github.com/go-sql-driver/mysql/blob/76c00e35a8d48f8f70f0e7dffe584692bd3fa612/packets.go#L598-L613
function isReadOnlyError(err) {
if (!err || !err.errno) {
return false;
}
// 1792: ER_CANT_EXECUTE_IN_READ_ONLY_TRANSACTION
// 1290: ER_OPTION_PREVENTS_STATEMENT (returned by Aurora during failover)
// 1836: ER_READ_ONLY_MODE
return (
err.errno === Errors.ER_OPTION_PREVENTS_STATEMENT ||
err.errno === Errors.ER_CANT_EXECUTE_IN_READ_ONLY_TRANSACTION ||
err.errno === Errors.ER_READ_ONLY_MODE
);
}
function spliceConnection(queue, connection) {
const len = queue.length;
for (let i = 0; i < len; i++) {
if (queue.get(i) === connection) {
queue.removeOne(i);
break;
}
}
}
class BasePool extends EventEmitter {
constructor(options) {
super();
this.config = options.config;
this.config.connectionConfig.pool = this;
this._allConnections = new Queue();
this._freeConnections = new Queue();
this._connectionQueue = new Queue();
this._closed = false;
if (this.config.maxIdle < this.config.connectionLimit) {
// create idle connection timeout automatically release job
this._removeIdleTimeoutConnections();
}
}
getConnection(cb) {
const _getConnection = (cb) => {
if (this._closed) {
return process.nextTick(() => cb(new Error('Pool is closed.')));
}
let connection;
if (this._freeConnections.length > 0) {
connection = this._freeConnections.pop();
this.emit('acquire', connection);
return process.nextTick(() => {
connection._released = false;
cb(null, connection);
});
}
if (
this.config.connectionLimit === 0 ||
this._allConnections.length < this.config.connectionLimit
) {
connection = new PoolConnection(this, {
config: this.config.connectionConfig,
});
this._allConnections.push(connection);
return connection.connect((err) => {
if (this._closed) {
return cb(new Error('Pool is closed.'));
}
if (err) {
return cb(err);
}
this.emit('connection', connection);
this.emit('acquire', connection);
return cb(null, connection);
});
}
if (!this.config.waitForConnections) {
return process.nextTick(() =>
cb(new Error('No connections available.'))
);
}
if (
this.config.queueLimit &&
this._connectionQueue.length >= this.config.queueLimit
) {
return cb(new Error('Queue limit reached.'));
}
this.emit('enqueue');
return this._connectionQueue.push(cb);
};
const config = this.config.connectionConfig;
traceCallback(
poolConnectChannel,
_getConnection,
0,
() => {
const server = getServerContext(config);
return {
database: config.database || '',
serverAddress: server.serverAddress,
serverPort: server.serverPort,
};
},
null,
cb
);
}
releaseConnection(connection) {
let cb;
if (!connection._pool) {
// The connection has been removed from the pool and is no longer good.
if (this._connectionQueue.length) {
cb = this._connectionQueue.shift();
process.nextTick(this.getConnection.bind(this, cb));
}
return;
}
// Reset connection state if configured
if (this.config.resetOnRelease && connection.reset) {
connection.reset((err) => {
if (err) {
// If reset fails, remove connection from pool
connection._pool = null;
spliceConnection(this._allConnections, connection);
connection.destroy();
// Try to create a new connection for waiting callbacks
if (this._connectionQueue.length) {
cb = this._connectionQueue.shift();
process.nextTick(this.getConnection.bind(this, cb));
}
return;
}
// Reset successful, continue with normal release flow
this._handleSuccessfulRelease(connection);
});
} else {
this._handleSuccessfulRelease(connection);
}
}
_handleSuccessfulRelease(connection) {
let cb;
if (this._connectionQueue.length) {
cb = this._connectionQueue.shift();
process.nextTick(() => {
connection._released = false;
cb(null, connection);
});
} else {
this._freeConnections.push(connection);
this.emit('release', connection);
}
}
[Symbol.dispose]() {
if (!this._closed) {
this.end();
}
}
end(cb) {
this._closed = true;
clearTimeout(this._removeIdleTimeoutConnectionsTimer);
if (typeof cb !== 'function') {
cb = function (err) {
if (err) {
throw err;
}
};
}
while (this._connectionQueue.length > 0) {
const queuedCallback = this._connectionQueue.shift();
process.nextTick(() => queuedCallback(new Error('Pool is closed.')));
}
let calledBack = false;
let closedConnections = 0;
let connection;
const endCB = function (err) {
if (calledBack) {
return;
}
if (err || ++closedConnections >= this._allConnections.length) {
calledBack = true;
cb(err);
return;
}
}.bind(this);
if (this._allConnections.length === 0) {
endCB();
return;
}
for (let i = 0; i < this._allConnections.length; i++) {
connection = this._allConnections.get(i);
connection._realEnd(endCB);
}
}
query(sql, values, cb) {
const cmdQuery = BaseConnection.createQuery(
sql,
values,
cb,
this.config.connectionConfig
);
if (typeof cmdQuery.namedPlaceholders === 'undefined') {
cmdQuery.namedPlaceholders =
this.config.connectionConfig.namedPlaceholders;
}
this.getConnection((err, conn) => {
if (err) {
if (typeof cmdQuery.onResult === 'function') {
cmdQuery.onResult(err);
} else {
cmdQuery.emit('error', err);
}
return;
}
try {
let queryError = null;
const origOnResult = cmdQuery.onResult;
if (origOnResult) {
cmdQuery.onResult = function (err, rows, fields) {
queryError = err || null;
origOnResult(err, rows, fields);
};
} else {
cmdQuery.once('error', (err) => {
queryError = err;
});
}
conn.query(cmdQuery).once('end', () => {
if (isReadOnlyError(queryError)) {
conn.destroy();
} else {
conn.release();
}
});
} catch (e) {
conn.release();
throw e;
}
});
return cmdQuery;
}
execute(sql, values, cb) {
// TODO construct execute command first here and pass it to connection.execute
// so that polymorphic arguments logic is there in one place
if (typeof values === 'function') {
cb = values;
values = [];
}
this.getConnection((err, conn) => {
if (err) {
return cb(err);
}
try {
conn
.execute(sql, values, (err, rows, fields) => {
if (isReadOnlyError(err)) {
conn.destroy();
}
cb(err, rows, fields);
})
.once('end', () => {
conn.release();
});
} catch (e) {
conn.release();
return cb(e);
}
});
}
_removeConnection(connection) {
// Remove connection from all connections
spliceConnection(this._allConnections, connection);
// Remove connection from free connections
spliceConnection(this._freeConnections, connection);
this.releaseConnection(connection);
}
_removeIdleTimeoutConnections() {
if (this._removeIdleTimeoutConnectionsTimer) {
clearTimeout(this._removeIdleTimeoutConnectionsTimer);
}
this._removeIdleTimeoutConnectionsTimer = setTimeout(() => {
try {
while (
this._freeConnections.length > this.config.maxIdle ||
(this._freeConnections.length > 0 &&
Date.now() - this._freeConnections.get(0).lastActiveTime >
this.config.idleTimeout)
) {
if (this.config.connectionConfig.gracefulEnd) {
this._freeConnections.get(0).end();
} else {
this._freeConnections.get(0).destroy();
}
}
} finally {
this._removeIdleTimeoutConnections();
}
}, 1000);
}
format(sql, values) {
return SqlString.format(
sql,
values,
this.config.connectionConfig.stringifyObjects,
this.config.connectionConfig.timezone
);
}
escape(value) {
return SqlString.escape(
value,
this.config.connectionConfig.stringifyObjects,
this.config.connectionConfig.timezone
);
}
escapeId(value) {
return SqlString.escapeId(value, false);
}
}
module.exports = BasePool;
+151
View File
@@ -0,0 +1,151 @@
// This file was modified by Oracle on July 5, 2021.
// Errors generated by asynchronous authentication plugins are now being
// handled and subsequently emitted at the command level.
// Modifications copyright (c) 2021, Oracle and/or its affiliates.
'use strict';
const Packets = require('../packets/index.js');
const sha256_password = require('../auth_plugins/sha256_password');
const caching_sha2_password = require('../auth_plugins/caching_sha2_password.js');
const mysql_native_password = require('../auth_plugins/mysql_native_password.js');
const mysql_clear_password = require('../auth_plugins/mysql_clear_password.js');
// Use Object.create(null) to avoid prototype pollution
// This prevents server-controlled pluginName values like "toString" or "__proto__"
// from resolving to prototype properties
const standardAuthPlugins = Object.assign(Object.create(null), {
sha256_password: sha256_password({}),
caching_sha2_password: caching_sha2_password({}),
mysql_native_password: mysql_native_password({}),
mysql_clear_password: mysql_clear_password({}),
});
// Helper function to get auth plugin (custom or standard)
function getAuthPlugin(pluginName, connection) {
const customPlugins = connection.config.authPlugins;
// Check custom plugins with hasOwnProperty for safety
if (
customPlugins &&
Object.prototype.hasOwnProperty.call(customPlugins, pluginName)
) {
return customPlugins[pluginName];
}
// Safe to access standardAuthPlugins directly since it has no prototype
return standardAuthPlugins[pluginName];
}
function warnLegacyAuthSwitch() {
console.warn(
'WARNING! authSwitchHandler api is deprecated, please use new authPlugins api'
);
}
function authSwitchPluginError(error, command) {
// Authentication errors are fatal
error.code = 'AUTH_SWITCH_PLUGIN_ERROR';
error.fatal = true;
command.emit('error', error);
}
function authSwitchRequest(packet, connection, command) {
const { pluginName, pluginData } =
Packets.AuthSwitchRequest.fromPacket(packet);
// legacy plugin api don't allow to override mysql_native_password
// if pluginName is mysql_native_password it's using standard auth4.1 auth
if (
connection.config.authSwitchHandler &&
pluginName !== 'mysql_native_password'
) {
const legacySwitchHandler = connection.config.authSwitchHandler;
warnLegacyAuthSwitch();
legacySwitchHandler({ pluginName, pluginData }, (err, data) => {
if (err) {
return authSwitchPluginError(err, command);
}
connection.writePacket(new Packets.AuthSwitchResponse(data).toPacket());
});
return;
}
if (pluginName === 'mysql_clear_password') {
const hasCustomPlugin =
connection.config.authPlugins &&
Object.prototype.hasOwnProperty.call(
connection.config.authPlugins,
'mysql_clear_password'
);
if (!hasCustomPlugin && !connection.config.enableCleartextPlugin) {
const err = new Error(
'Server requested authentication using mysql_clear_password, ' +
'which sends the password in plaintext over the network and is ' +
'disabled by default. To enable it, set the `enableCleartextPlugin` ' +
'option to `true` in your connection configuration, or provide a ' +
'custom `mysql_clear_password` auth plugin via the `authPlugins` ' +
'option. Only use this over a secure connection (TLS/SSL).'
);
err.code = 'MYSQL_CLEAR_PASSWORD_NOT_ENABLED';
err.fatal = true;
throw err;
}
}
const authPlugin = getAuthPlugin(pluginName, connection);
if (!authPlugin) {
throw new Error(
`Server requests authentication using unknown plugin ${pluginName}. See ${'TODO: add plugins doco here'} on how to configure or author authentication plugins.`
);
}
connection._authPlugin = authPlugin({ connection, command });
Promise.resolve(connection._authPlugin(pluginData))
.then((data) => {
if (data) {
connection.writePacket(new Packets.AuthSwitchResponse(data).toPacket());
}
})
.catch((err) => {
authSwitchPluginError(err, command);
});
}
function authSwitchRequestMoreData(packet, connection, command) {
const { data } = Packets.AuthSwitchRequestMoreData.fromPacket(packet);
if (connection.config.authSwitchHandler) {
const legacySwitchHandler = connection.config.authSwitchHandler;
warnLegacyAuthSwitch();
legacySwitchHandler({ pluginData: data }, (err, data) => {
if (err) {
return authSwitchPluginError(err, command);
}
connection.writePacket(new Packets.AuthSwitchResponse(data).toPacket());
});
return;
}
if (!connection._authPlugin) {
throw new Error(
'AuthPluginMoreData received but no auth plugin instance found'
);
}
Promise.resolve(connection._authPlugin(data))
.then((data) => {
if (data) {
connection.writePacket(new Packets.AuthSwitchResponse(data).toPacket());
}
})
.catch((err) => {
authSwitchPluginError(err, command);
});
}
module.exports = {
authSwitchRequest,
authSwitchRequestMoreData,
getAuthPlugin,
standardAuthPlugins,
};
+109
View File
@@ -0,0 +1,109 @@
'use strict';
const Command = require('./command');
const Packets = require('../packets');
const eventParsers = [];
class BinlogEventHeader {
constructor(packet) {
this.timestamp = packet.readInt32();
this.eventType = packet.readInt8();
this.serverId = packet.readInt32();
this.eventSize = packet.readInt32();
this.logPos = packet.readInt32();
this.flags = packet.readInt16();
}
}
class BinlogDump extends Command {
constructor(opts) {
super();
// this.onResult = callback;
this.opts = opts;
}
start(packet, connection) {
const newPacket = new Packets.BinlogDump(this.opts);
connection.writePacket(newPacket.toPacket(1));
return BinlogDump.prototype.binlogData;
}
binlogData(packet) {
// ok - continue consuming events
// error - error
// eof - end of binlog
if (packet.isEOF()) {
this.emit('eof');
return null;
}
// binlog event header
packet.readInt8();
const header = new BinlogEventHeader(packet);
const EventParser = eventParsers[header.eventType];
let event;
if (EventParser) {
event = new EventParser(packet);
} else {
event = {
name: 'UNKNOWN',
};
}
event.header = header;
this.emit('event', event);
return BinlogDump.prototype.binlogData;
}
}
class RotateEvent {
constructor(packet) {
this.pposition = packet.readInt32();
// TODO: read uint64 here
packet.readInt32(); // positionDword2
this.nextBinlog = packet.readString();
this.name = 'RotateEvent';
}
}
class FormatDescriptionEvent {
constructor(packet) {
this.binlogVersion = packet.readInt16();
this.serverVersion = packet.readString(50).replace(/\u0000.*/, ''); // eslint-disable-line no-control-regex
this.createTimestamp = packet.readInt32();
this.eventHeaderLength = packet.readInt8(); // should be 19
this.eventsLength = packet.readBuffer();
this.name = 'FormatDescriptionEvent';
}
}
class QueryEvent {
constructor(packet) {
const parseStatusVars = require('../packets/binlog_query_statusvars.js');
this.slaveProxyId = packet.readInt32();
this.executionTime = packet.readInt32();
const schemaLength = packet.readInt8();
this.errorCode = packet.readInt16();
const statusVarsLength = packet.readInt16();
const statusVars = packet.readBuffer(statusVarsLength);
this.schema = packet.readString(schemaLength);
packet.readInt8(); // should be zero
this.statusVars = parseStatusVars(statusVars);
this.query = packet.readString();
this.name = 'QueryEvent';
}
}
class XidEvent {
constructor(packet) {
this.binlogVersion = packet.readInt16();
this.xid = packet.readInt64();
this.name = 'XidEvent';
}
}
eventParsers[2] = QueryEvent;
eventParsers[4] = RotateEvent;
eventParsers[15] = FormatDescriptionEvent;
eventParsers[16] = XidEvent;
module.exports = BinlogDump;
+68
View File
@@ -0,0 +1,68 @@
// This file was modified by Oracle on September 21, 2021.
// The changes involve saving additional authentication factor passwords
// in the command scope and enabling multi-factor authentication in the
// client-side when the server supports it.
// Modifications copyright (c) 2021, Oracle and/or its affiliates.
'use strict';
const Command = require('./command.js');
const Packets = require('../packets/index.js');
const ClientConstants = require('../constants/client');
const ClientHandshake = require('./client_handshake.js');
const CharsetToEncoding = require('../constants/charset_encodings.js');
class ChangeUser extends Command {
constructor(options, callback) {
super();
this.onResult = callback;
this.user = options.user;
this.password = options.password;
// "password1" is an alias of "password"
this.password1 = options.password;
this.password2 = options.password2;
this.password3 = options.password3;
this.database = options.database;
this.passwordSha1 = options.passwordSha1;
this.charsetNumber = options.charsetNumber;
this.currentConfig = options.currentConfig;
this.authenticationFactor = 0;
}
start(packet, connection) {
const newPacket = new Packets.ChangeUser({
flags: connection.config.clientFlags,
user: this.user,
database: this.database,
charsetNumber: this.charsetNumber,
password: this.password,
passwordSha1: this.passwordSha1,
authPluginData1: connection._handshakePacket.authPluginData1,
authPluginData2: connection._handshakePacket.authPluginData2,
});
this.currentConfig.user = this.user;
this.currentConfig.password = this.password;
this.currentConfig.database = this.database;
this.currentConfig.charsetNumber = this.charsetNumber;
connection.clientEncoding = CharsetToEncoding[this.charsetNumber];
// clear prepared statements cache as all statements become invalid after changeUser
connection._statements.clear();
connection.writePacket(newPacket.toPacket());
// check if the server supports multi-factor authentication
const multiFactorAuthentication =
connection.serverCapabilityFlags &
ClientConstants.MULTI_FACTOR_AUTHENTICATION;
if (multiFactorAuthentication) {
// if the server supports multi-factor authentication, we enable it in
// the client
this.authenticationFactor = 1;
}
return ChangeUser.prototype.handshakeResult;
}
}
ChangeUser.prototype.handshakeResult =
ClientHandshake.prototype.handshakeResult;
ChangeUser.prototype.calculateNativePasswordAuthToken =
ClientHandshake.prototype.calculateNativePasswordAuthToken;
module.exports = ChangeUser;
+386
View File
@@ -0,0 +1,386 @@
// This file was modified by Oracle on June 17, 2021.
// Handshake errors are now maked as fatal and the corresponding events are
// emitted in the command instance itself.
// Modifications copyright (c) 2021, Oracle and/or its affiliates.
// This file was modified by Oracle on September 21, 2021.
// Handshake workflow now supports additional authentication factors requested
// by the server.
// Modifications copyright (c) 2021, Oracle and/or its affiliates.
'use strict';
const Command = require('./command.js');
const Packets = require('../packets/index.js');
const ClientConstants = require('../constants/client.js');
const CharsetToEncoding = require('../constants/charset_encodings.js');
const auth41 = require('../auth_41.js');
const { getAuthPlugin } = require('./auth_switch.js');
const {
calculateToken: calculateSha2Token,
} = require('../auth_plugins/caching_sha2_password.js');
function flagNames(flags) {
const res = [];
for (const c in ClientConstants) {
if (flags & ClientConstants[c]) {
res.push(c.replace(/_/g, ' ').toLowerCase());
}
}
return res;
}
class ClientHandshake extends Command {
constructor(clientFlags) {
super();
this.handshake = null;
this.clientFlags = clientFlags;
this.authenticationFactor = 0;
}
start() {
return ClientHandshake.prototype.handshakeInit;
}
sendSSLRequest(connection) {
const sslRequest = new Packets.SSLRequest(
this.clientFlags,
connection.config.charsetNumber
);
connection.writePacket(sslRequest.toPacket());
}
sendCredentials(connection) {
if (connection.config.debug) {
console.log(
'Sending handshake packet: flags:%d=(%s)',
this.clientFlags,
flagNames(this.clientFlags).join(', ')
);
}
this.user = connection.config.user;
this.password = connection.config.password;
// "password1" is an alias to the original "password" value
// to make it easier to integrate multi-factor authentication
this.password1 = connection.config.password;
// "password2" and "password3" are the 2nd and 3rd factor authentication
// passwords, which can be undefined depending on the authentication
// plugin being used
this.password2 = connection.config.password2;
this.password3 = connection.config.password3;
this.passwordSha1 = connection.config.passwordSha1;
this.database = connection.config.database;
this.authPluginName = this.handshake.authPluginName;
// Optimization: Try to use the server's preferred authentication method
// to avoid an unnecessary auth switch roundtrip
const serverAuthMethod = this.handshake.authPluginName;
const isSecureConnection =
connection.config.ssl || connection.config.socketPath;
// Combine auth plugin data for easier handling
// Note: authPluginData2 can include a trailing NUL byte when PLUGIN_AUTH is set
// We must ensure exactly 20 bytes for the scramble
const authPluginData =
this.handshake.authPluginData1 && this.handshake.authPluginData2
? Buffer.concat([
this.handshake.authPluginData1,
this.handshake.authPluginData2,
]).slice(0, 20)
: Buffer.alloc(20);
// Check if user has custom auth plugin or legacy handler for the server-advertised method
// If so, we must not bypass the auth switch flow with our built-in implementation
const hasCustomAuthPlugin =
connection.config.authPlugins &&
Object.prototype.hasOwnProperty.call(
connection.config.authPlugins,
serverAuthMethod
);
const hasLegacyAuthSwitchHandler =
typeof connection.config.authSwitchHandler === 'function';
// Determine which auth method to use
// Try to use server's preferred method if we can, otherwise fallback to native
const canUseDirectAuth =
!hasCustomAuthPlugin &&
!hasLegacyAuthSwitchHandler &&
this.canUseAuthMethodDirectly(serverAuthMethod, isSecureConnection) &&
(serverAuthMethod !== 'mysql_clear_password' ||
connection.config.enableCleartextPlugin);
const clientAuthMethod = canUseDirectAuth
? serverAuthMethod
: 'mysql_native_password';
// Calculate the auth token for the chosen method
const authToken = this.calculateAuthToken(
clientAuthMethod,
this.password,
authPluginData
);
if (connection.config.debug) {
console.log(
'Server auth method: %s, Using auth method: %s',
serverAuthMethod,
clientAuthMethod
);
}
const handshakeResponse = new Packets.HandshakeResponse({
flags: this.clientFlags,
user: this.user,
database: this.database,
password: this.password,
passwordSha1: this.passwordSha1,
charsetNumber: connection.config.charsetNumber,
authPluginData1: this.handshake.authPluginData1,
authPluginData2: this.handshake.authPluginData2,
compress: connection.config.compress,
connectAttributes: connection.config.connectAttributes,
authToken: authToken,
authPluginName: clientAuthMethod,
});
connection.writePacket(handshakeResponse.toPacket());
// If we used a non-native auth method in the initial handshake response,
// we need to prepare for potential AuthMoreData packets by creating
// the appropriate auth plugin instance
if (clientAuthMethod !== 'mysql_native_password') {
this.initializeAuthPlugin(clientAuthMethod, authPluginData, connection);
}
}
calculateNativePasswordAuthToken(authPluginData) {
// TODO: dont split into authPluginData1 and authPluginData2, instead join when 1 & 2 received
const authPluginData1 = authPluginData.slice(0, 8);
const authPluginData2 = authPluginData.slice(8, 20);
let authToken;
if (this.passwordSha1) {
authToken = auth41.calculateTokenFromPasswordSha(
this.passwordSha1,
authPluginData1,
authPluginData2
);
} else {
authToken = auth41.calculateToken(
this.password,
authPluginData1,
authPluginData2
);
}
return authToken;
}
calculateSha256Token(password, scramble) {
// Reuse the token calculation from caching_sha2_password plugin
// to avoid code duplication and ensure consistency
return calculateSha2Token(password, scramble);
}
// Helper: Calculate auth token for a specific auth method
calculateAuthToken(authMethod, password, authPluginData) {
switch (authMethod) {
case 'mysql_native_password':
return this.calculateNativePasswordAuthToken(authPluginData);
case 'caching_sha2_password':
return this.calculateSha256Token(password, authPluginData);
case 'sha256_password':
case 'mysql_clear_password':
// These methods send plaintext password over secure connections
return password
? Buffer.from(`${password}\0`, 'utf8')
: Buffer.alloc(0);
default:
// Unknown method - use native password as fallback
return this.calculateNativePasswordAuthToken(authPluginData);
}
}
// Helper: Determine if we can use a specific auth method directly
canUseAuthMethodDirectly(authMethod, isSecureConnection) {
switch (authMethod) {
case 'mysql_native_password':
case 'caching_sha2_password':
// These methods work with or without SSL
return true;
case 'sha256_password':
case 'mysql_clear_password':
// These methods require secure connection for direct use
return isSecureConnection;
default:
// Unknown methods - fallback to native password
return false;
}
}
// Helper: Initialize auth plugin for handling subsequent AuthMoreData packets
initializeAuthPlugin(authMethod, authPluginData, connection) {
const authPlugin = getAuthPlugin(authMethod, connection);
if (!authPlugin) {
return; // Plugin not found, will fallback to auth switch if needed
}
// Initialize the plugin with connection and command context
const pluginHandler = authPlugin({ connection, command: this });
connection._authPlugin = pluginHandler;
// Prime the plugin by calling it with the scramble data
// This advances the plugin's state machine (e.g., to STATE_TOKEN_SENT)
// We don't send the result because we already included it in the handshake response
try {
Promise.resolve(pluginHandler(authPluginData)).catch((err) => {
// Ignore errors during initialization since we already sent the token
if (connection.config.debug) {
console.log('Auth plugin initialization:', err.message);
}
});
} catch (err) {
// Ignore synchronous errors during initialization
if (connection.config.debug) {
console.log('Auth plugin initialization error:', err.message);
}
}
}
handshakeInit(helloPacket, connection) {
this.on('error', (e) => {
connection._fatalError = e;
connection._protocolError = e;
});
this.handshake = Packets.Handshake.fromPacket(helloPacket);
if (connection.config.debug) {
console.log(
'Server hello packet: capability flags:%d=(%s)',
this.handshake.capabilityFlags,
flagNames(this.handshake.capabilityFlags).join(', ')
);
}
connection.serverCapabilityFlags = this.handshake.capabilityFlags;
connection.serverEncoding = CharsetToEncoding[this.handshake.characterSet];
connection.connectionId = this.handshake.connectionId;
const serverSSLSupport =
this.handshake.capabilityFlags & ClientConstants.SSL;
// multi factor authentication is enabled with the
// "MULTI_FACTOR_AUTHENTICATION" capability and should only be used if it
// is supported by the server
const multiFactorAuthentication =
this.handshake.capabilityFlags &
ClientConstants.MULTI_FACTOR_AUTHENTICATION;
this.clientFlags = this.clientFlags | multiFactorAuthentication;
// use compression only if requested by client and supported by server
connection.config.compress =
connection.config.compress &&
this.handshake.capabilityFlags & ClientConstants.COMPRESS;
this.clientFlags = this.clientFlags | connection.config.compress;
if (connection.config.ssl) {
// client requires SSL but server does not support it
if (!serverSSLSupport) {
const err = new Error('Server does not support secure connection');
err.code = 'HANDSHAKE_NO_SSL_SUPPORT';
err.fatal = true;
this.emit('error', err);
return false;
}
// send ssl upgrade request and immediately upgrade connection to secure
this.clientFlags |= ClientConstants.SSL;
this.sendSSLRequest(connection);
connection.startTLS((err) => {
// after connection is secure
if (err) {
// SSL negotiation error are fatal
err.code = 'HANDSHAKE_SSL_ERROR';
err.fatal = true;
this.emit('error', err);
return;
}
// rest of communication is encrypted
this.sendCredentials(connection);
});
} else {
this.sendCredentials(connection);
}
if (multiFactorAuthentication) {
// if the server supports multi-factor authentication, we enable it in
// the client
this.authenticationFactor = 1;
}
return ClientHandshake.prototype.handshakeResult;
}
handshakeResult(packet, connection) {
const marker = packet.peekByte();
// packet can be OK_Packet, ERR_Packet, AuthSwitchRequest, AuthNextFactor
// or AuthMoreData
if (marker === 0xfe || marker === 1 || marker === 0x02) {
const authSwitch = require('./auth_switch');
try {
if (marker === 1) {
authSwitch.authSwitchRequestMoreData(packet, connection, this);
} else {
// if authenticationFactor === 0, it means the server does not support
// the multi-factor authentication capability
if (this.authenticationFactor !== 0) {
// if we are past the first authentication factor, we should use the
// corresponding password (if there is one)
connection.config.password =
this[`password${this.authenticationFactor}`];
// update the current authentication factor
this.authenticationFactor += 1;
}
// if marker === 0x02, it means it is an AuthNextFactor packet,
// which is similar in structure to an AuthSwitchRequest packet,
// so, we can use it directly
authSwitch.authSwitchRequest(packet, connection, this);
}
return ClientHandshake.prototype.handshakeResult;
} catch (err) {
if (!err.code) {
err.code = 'AUTH_SWITCH_PLUGIN_ERROR';
}
err.fatal = true;
if (this.onResult) {
this.onResult(err);
} else {
this.emit('error', err);
}
return null;
}
}
if (marker !== 0) {
const err = new Error('Unexpected packet during handshake phase');
// Unknown handshake errors are fatal
err.code = 'HANDSHAKE_UNKNOWN_ERROR';
err.fatal = true;
if (this.onResult) {
this.onResult(err);
} else {
this.emit('error', err);
}
return null;
}
// this should be called from ClientHandshake command only
// and skipped when called from ChangeUser command
if (!connection.authorized) {
connection.authorized = true;
if (connection.config.compress) {
const enableCompression =
require('../compressed_protocol.js').enableCompression;
enableCompression(connection);
}
}
if (this.onResult) {
this.onResult(null);
}
return null;
}
}
module.exports = ClientHandshake;
+18
View File
@@ -0,0 +1,18 @@
'use strict';
const Command = require('./command');
const Packets = require('../packets/index.js');
class CloseStatement extends Command {
constructor(id) {
super();
this.id = id;
}
start(packet, connection) {
connection.writePacket(new Packets.CloseStatement(this.id).toPacket(1));
return null;
}
}
module.exports = CloseStatement;
+54
View File
@@ -0,0 +1,54 @@
'use strict';
const EventEmitter = require('events').EventEmitter;
const Timers = require('timers');
class Command extends EventEmitter {
constructor() {
super();
this.next = null;
}
// slow. debug only
stateName() {
const state = this.next;
for (const i in this) {
if (this[i] === state && i !== 'next') {
return i;
}
}
return 'unknown name';
}
execute(packet, connection) {
if (!this.next) {
this.next = this.start;
connection._resetSequenceId();
}
if (packet && packet.isError()) {
const err = packet.asError(connection.clientEncoding);
err.sql = this.sql || this.query;
if (this.queryTimeout) {
Timers.clearTimeout(this.queryTimeout);
this.queryTimeout = null;
}
if (this.onResult) {
this.onResult(err);
this.emit('end');
} else {
this.emit('error', err);
this.emit('end');
}
return true;
}
// TODO: don't return anything from execute, it's ugly and error-prone. Listen for 'end' event in connection
this.next = this.next(packet, connection);
if (this.next) {
return false;
}
this.emit('end');
return true;
}
}
module.exports = Command;
+116
View File
@@ -0,0 +1,116 @@
'use strict';
const Command = require('./command.js');
const Query = require('./query.js');
const Packets = require('../packets/index.js');
const getBinaryParser = require('../parsers/binary_parser.js');
const getStaticBinaryParser = require('../parsers/static_binary_parser.js');
class Execute extends Command {
constructor(options, callback) {
super();
this.statement = options.statement;
this.sql = options.sql;
this.values = options.values;
this.onResult = callback;
this.parameters = options.values;
this.insertId = 0;
this.timeout = options.timeout;
this.queryTimeout = null;
this._rows = [];
this._fields = [];
this._result = [];
this._fieldCount = 0;
this._rowParser = null;
this._executeOptions = options;
this._resultIndex = 0;
this._localStream = null;
this._unpipeStream = function () {};
this._streamFactory = options.infileStreamFactory;
this._connection = null;
}
buildParserFromFields(fields, connection) {
if (this.options.disableEval) {
return getStaticBinaryParser(fields, this.options, connection.config);
}
return getBinaryParser(fields, this.options, connection.config);
}
start(packet, connection) {
this._connection = connection;
this.options = Object.assign({}, connection.config, this._executeOptions);
this._setTimeout();
const clientFlags =
connection.config.clientFlags & (connection.serverCapabilityFlags || 0);
const executePacket = new Packets.Execute(
this.statement.id,
this.parameters,
connection.config.charsetNumber,
connection.config.timezone,
this._executeOptions.attributes,
clientFlags
);
//For reasons why this try-catch is here, please see
// https://github.com/sidorares/node-mysql2/pull/689
//For additional discussion, see
// 1. https://github.com/sidorares/node-mysql2/issues/493
// 2. https://github.com/sidorares/node-mysql2/issues/187
// 3. https://github.com/sidorares/node-mysql2/issues/480
try {
connection.writePacket(executePacket.toPacket(1));
} catch (error) {
this.onResult(error);
}
return Execute.prototype.resultsetHeader;
}
readField(packet, connection) {
let fields;
// disabling for now, but would be great to find reliable way to parse fields only once
// fields reported by prepare can be empty at all or just incorrect - see #169
//
// perfomance optimisation: if we already have this field parsed in statement header, use one from header
// const field = this.statement.columns.length == this._fieldCount ?
// this.statement.columns[this._receivedFieldsCount] : new Packets.ColumnDefinition(packet);
const field = new Packets.ColumnDefinition(
packet,
connection.clientEncoding
);
this._receivedFieldsCount++;
this._fields[this._resultIndex].push(field);
if (this._receivedFieldsCount === this._fieldCount) {
fields = this._fields[this._resultIndex];
this.emit('fields', fields, this._resultIndex);
return Execute.prototype.fieldsEOF;
}
return Execute.prototype.readField;
}
fieldsEOF(packet, connection) {
// check EOF
if (!packet.isEOF()) {
return connection.protocolError('Expected EOF packet');
}
this._rowParser = new (this.buildParserFromFields(
this._fields[this._resultIndex],
connection
))();
return Execute.prototype.row;
}
}
Execute.prototype.done = Query.prototype.done;
Execute.prototype.doneInsert = Query.prototype.doneInsert;
Execute.prototype.resultsetHeader = Query.prototype.resultsetHeader;
Execute.prototype._findOrCreateReadStream =
Query.prototype._findOrCreateReadStream;
Execute.prototype._streamLocalInfile = Query.prototype._streamLocalInfile;
Execute.prototype._setTimeout = Query.prototype._setTimeout;
Execute.prototype._handleTimeoutError = Query.prototype._handleTimeoutError;
Execute.prototype.row = Query.prototype.row;
Execute.prototype.stream = Query.prototype.stream;
module.exports = Execute;
+29
View File
@@ -0,0 +1,29 @@
'use strict';
const ClientHandshake = require('./client_handshake.js');
const ServerHandshake = require('./server_handshake.js');
const Query = require('./query.js');
const Prepare = require('./prepare.js');
const CloseStatement = require('./close_statement.js');
const Execute = require('./execute.js');
const Ping = require('./ping.js');
const RegisterSlave = require('./register_slave.js');
const BinlogDump = require('./binlog_dump.js');
const ChangeUser = require('./change_user.js');
const ResetConnection = require('./reset_connection.js');
const Quit = require('./quit.js');
module.exports = {
ClientHandshake,
ServerHandshake,
Query,
Prepare,
CloseStatement,
Execute,
Ping,
RegisterSlave,
BinlogDump,
ChangeUser,
ResetConnection,
Quit,
};
+36
View File
@@ -0,0 +1,36 @@
'use strict';
const Command = require('./command');
const CommandCode = require('../constants/commands');
const Packet = require('../packets/packet');
// TODO: time statistics?
// usefull for queue size and network latency monitoring
// store created,sent,reply timestamps
class Ping extends Command {
constructor(callback) {
super();
this.onResult = callback;
}
start(packet, connection) {
const ping = new Packet(
0,
Buffer.from([1, 0, 0, 0, CommandCode.PING]),
0,
5
);
connection.writePacket(ping);
return Ping.prototype.pingResponse;
}
pingResponse() {
// TODO: check it's OK packet. error check already done in caller
if (this.onResult) {
process.nextTick(this.onResult.bind(this));
}
return null;
}
}
module.exports = Ping;
+143
View File
@@ -0,0 +1,143 @@
'use strict';
const Packets = require('../packets/index.js');
const Command = require('./command.js');
const CloseStatement = require('./close_statement.js');
const Execute = require('./execute.js');
class PreparedStatementInfo {
constructor(query, id, columns, parameters, connection) {
this.query = query;
this.id = id;
this.columns = columns;
this.parameters = parameters;
this.rowParser = null;
this._connection = connection;
}
close() {
return this._connection.addCommand(new CloseStatement(this.id));
}
execute(parameters, callback) {
if (typeof parameters === 'function') {
callback = parameters;
parameters = [];
}
return this._connection.addCommand(
new Execute({ statement: this, values: parameters }, callback)
);
}
}
class Prepare extends Command {
constructor(options, callback) {
super();
this.query = options.sql;
this.onResult = callback;
this.id = 0;
this.fieldCount = 0;
this.parameterCount = 0;
this.fields = [];
this.parameterDefinitions = [];
this.options = options;
}
start(packet, connection) {
const Connection = connection.constructor;
this.key = Connection.statementKey(this.options);
const statement = connection._statements.get(this.key);
if (statement) {
if (this.onResult) {
this.onResult(null, statement);
}
return null;
}
const cmdPacket = new Packets.PrepareStatement(
this.query,
connection.config.charsetNumber,
this.options.values
);
connection.writePacket(cmdPacket.toPacket(1));
return Prepare.prototype.prepareHeader;
}
prepareHeader(packet, connection) {
const header = new Packets.PreparedStatementHeader(packet);
this.id = header.id;
this.fieldCount = header.fieldCount;
this.parameterCount = header.parameterCount;
if (this.parameterCount > 0) {
return Prepare.prototype.readParameter;
}
if (this.fieldCount > 0) {
return Prepare.prototype.readField;
}
return this.prepareDone(connection);
}
readParameter(packet, connection) {
// there might be scenarios when mysql server reports more parameters than
// are actually present in the array of parameter definitions.
// if EOF packet is received we switch to "read fields" state if there are
// any fields reported by the server, otherwise we finish the command.
if (packet.isEOF()) {
if (this.fieldCount > 0) {
return Prepare.prototype.readField;
}
return this.prepareDone(connection);
}
const def = new Packets.ColumnDefinition(packet, connection.clientEncoding);
this.parameterDefinitions.push(def);
if (this.parameterDefinitions.length === this.parameterCount) {
return Prepare.prototype.parametersEOF;
}
return this.readParameter;
}
readField(packet, connection) {
if (packet.isEOF()) {
return this.prepareDone(connection);
}
const def = new Packets.ColumnDefinition(packet, connection.clientEncoding);
this.fields.push(def);
if (this.fields.length === this.fieldCount) {
return Prepare.prototype.fieldsEOF;
}
return Prepare.prototype.readField;
}
parametersEOF(packet, connection) {
if (!packet.isEOF()) {
return connection.protocolError('Expected EOF packet after parameters');
}
if (this.fieldCount > 0) {
return Prepare.prototype.readField;
}
return this.prepareDone(connection);
}
fieldsEOF(packet, connection) {
if (!packet.isEOF()) {
return connection.protocolError('Expected EOF packet after fields');
}
return this.prepareDone(connection);
}
prepareDone(connection) {
const statement = new PreparedStatementInfo(
this.query,
this.id,
this.fields,
this.parameterDefinitions,
connection
);
connection._statements.set(this.key, statement);
if (this.onResult) {
this.onResult(null, statement);
}
return null;
}
}
module.exports = Prepare;
+365
View File
@@ -0,0 +1,365 @@
'use strict';
const process = require('process');
const Timers = require('timers');
const Readable = require('stream').Readable;
const Command = require('./command.js');
const Packets = require('../packets/index.js');
const getTextParser = require('../parsers/text_parser.js');
const staticParser = require('../parsers/static_text_parser.js');
const ServerStatus = require('../constants/server_status.js');
const EmptyPacket = new Packets.Packet(0, Buffer.allocUnsafe(4), 0, 4);
// http://dev.mysql.com/doc/internals/en/com-query.html
class Query extends Command {
constructor(options, callback) {
super();
this.sql = options.sql;
this.values = options.values;
this._queryOptions = options;
this.namedPlaceholders = options.namedPlaceholders || false;
this.onResult = callback;
this.timeout = options.timeout;
this.queryTimeout = null;
this._fieldCount = 0;
this._rowParser = null;
this._fields = [];
this._rows = [];
this._receivedFieldsCount = 0;
this._resultIndex = 0;
this._localStream = null;
this._unpipeStream = function () {};
this._streamFactory = options.infileStreamFactory;
this._connection = null;
}
then() {
const err =
"You have tried to call .then(), .catch(), or invoked await on the result of query that is not a promise, which is a programming error. Try calling con.promise().query(), or require('mysql2/promise') instead of 'mysql2' for a promise-compatible version of the query interface. To learn how to use async/await or Promises check out documentation at https://sidorares.github.io/node-mysql2/docs#using-promise-wrapper, or the mysql2 documentation at https://sidorares.github.io/node-mysql2/docs/documentation/promise-wrapper";
console.log(err);
throw new Error(err);
}
/* eslint no-unused-vars: ["error", { "argsIgnorePattern": "^_" }] */
start(_packet, connection) {
if (connection.config.debug) {
console.log(' Sending query command: %s', this.sql);
}
this._connection = connection;
this.options = Object.assign({}, connection.config, this._queryOptions);
this._setTimeout();
const clientFlags =
connection.config.clientFlags & (connection.serverCapabilityFlags || 0);
const cmdPacket = new Packets.Query(
this.sql,
connection.config.charsetNumber,
this._queryOptions.attributes,
clientFlags
);
connection.writePacket(cmdPacket.toPacket(1));
return Query.prototype.resultsetHeader;
}
done() {
this._unpipeStream();
// if all ready timeout, return null directly
if (this.timeout && !this.queryTimeout) {
return null;
}
// else clear timer
if (this.queryTimeout) {
Timers.clearTimeout(this.queryTimeout);
this.queryTimeout = null;
}
if (this.onResult) {
let rows, fields;
if (this._resultIndex === 0) {
rows = this._rows[0];
fields = this._fields[0];
} else {
rows = this._rows;
fields = this._fields;
}
if (fields) {
process.nextTick(() => {
this.onResult(null, rows, fields);
});
} else {
process.nextTick(() => {
this.onResult(null, rows);
});
}
}
return null;
}
doneInsert(rs) {
if (this._localStreamError) {
if (this.onResult) {
this.onResult(this._localStreamError, rs);
} else {
this.emit('error', this._localStreamError);
}
return null;
}
this._rows.push(rs);
this._fields.push(void 0);
this.emit('fields', void 0);
this.emit('result', rs);
if (rs.serverStatus & ServerStatus.SERVER_MORE_RESULTS_EXISTS) {
this._resultIndex++;
return this.resultsetHeader;
}
return this.done();
}
resultsetHeader(packet, connection) {
const rs = new Packets.ResultSetHeader(packet, connection);
this._fieldCount = rs.fieldCount;
if (connection.config.debug) {
console.log(
` Resultset header received, expecting ${rs.fieldCount} column definition packets`
);
}
if (this._fieldCount === 0) {
return this.doneInsert(rs);
}
if (this._fieldCount === null) {
return this._streamLocalInfile(connection, rs.infileName);
}
this._receivedFieldsCount = 0;
this._rows.push([]);
this._fields.push([]);
return this.readField;
}
_streamLocalInfile(connection, path) {
if (this._streamFactory) {
this._localStream = this._streamFactory(path);
} else {
this._localStreamError = new Error(
`As a result of LOCAL INFILE command server wants to read ${path} file, but as of v2.0 you must provide streamFactory option returning ReadStream.`
);
connection.writePacket(EmptyPacket);
return this.infileOk;
}
const onConnectionError = () => {
this._unpipeStream();
};
const onDrain = () => {
this._localStream.resume();
};
const onPause = () => {
this._localStream.pause();
};
const onData = function (data) {
const dataWithHeader = Buffer.allocUnsafe(data.length + 4);
data.copy(dataWithHeader, 4);
connection.writePacket(
new Packets.Packet(0, dataWithHeader, 0, dataWithHeader.length)
);
};
const onEnd = () => {
connection.removeListener('error', onConnectionError);
connection.writePacket(EmptyPacket);
};
const onError = (err) => {
this._localStreamError = err;
connection.removeListener('error', onConnectionError);
connection.writePacket(EmptyPacket);
};
this._unpipeStream = () => {
connection.stream.removeListener('pause', onPause);
connection.stream.removeListener('drain', onDrain);
this._localStream.removeListener('data', onData);
this._localStream.removeListener('end', onEnd);
this._localStream.removeListener('error', onError);
};
connection.stream.on('pause', onPause);
connection.stream.on('drain', onDrain);
this._localStream.on('data', onData);
this._localStream.on('end', onEnd);
this._localStream.on('error', onError);
connection.once('error', onConnectionError);
return this.infileOk;
}
readField(packet, connection) {
this._receivedFieldsCount++;
// Often there is much more data in the column definition than in the row itself
// If you set manually _fields[0] to array of ColumnDefinition's (from previous call)
// you can 'cache' result of parsing. Field packets still received, but ignored in that case
// this is the reason _receivedFieldsCount exist (otherwise we could just use current length of fields array)
if (this._fields[this._resultIndex].length !== this._fieldCount) {
const field = new Packets.ColumnDefinition(
packet,
connection.clientEncoding
);
this._fields[this._resultIndex].push(field);
if (connection.config.debug) {
console.log(' Column definition:');
console.log(` name: ${field.name}`);
console.log(` type: ${field.columnType}`);
console.log(` flags: ${field.flags}`);
}
}
// last field received
if (this._receivedFieldsCount === this._fieldCount) {
const fields = this._fields[this._resultIndex];
this.emit('fields', fields);
if (this.options.disableEval) {
this._rowParser = staticParser(fields, this.options, connection.config);
} else {
this._rowParser = new (getTextParser(
fields,
this.options,
connection.config
))(fields);
}
return Query.prototype.fieldsEOF;
}
return Query.prototype.readField;
}
fieldsEOF(packet, connection) {
// check EOF
if (!packet.isEOF()) {
return connection.protocolError('Expected EOF packet');
}
return this.row;
}
row(packet, _connection) {
if (packet.isEOF()) {
const status = packet.eofStatusFlags();
const moreResults = status & ServerStatus.SERVER_MORE_RESULTS_EXISTS;
if (moreResults) {
this._resultIndex++;
return Query.prototype.resultsetHeader;
}
return this.done();
}
let row;
try {
row = this._rowParser.next(
packet,
this._fields[this._resultIndex],
this.options
);
} catch (err) {
this._localStreamError = err;
return this.doneInsert(null);
}
if (this.onResult) {
this._rows[this._resultIndex].push(row);
} else {
this.emit('result', row, this._resultIndex);
}
return Query.prototype.row;
}
infileOk(packet, connection) {
const rs = new Packets.ResultSetHeader(packet, connection);
return this.doneInsert(rs);
}
stream(options) {
options = options || Object.create(null);
options.objectMode = true;
const stream = new Readable({
...options,
emitClose: true,
autoDestroy: true,
read: () => {
this._connection && this._connection.resume();
},
});
// Prevent a breaking change for users that rely on `end` event
stream.once('close', () => {
if (!stream.readableEnded) {
stream.emit('end');
}
});
const onResult = (row, index) => {
if (stream.destroyed) return;
if (!stream.push(row)) {
this._connection && this._connection.pause();
}
stream.emit('result', row, index); // replicate old emitter
};
const onFields = (fields) => {
if (stream.destroyed) return;
stream.emit('fields', fields); // replicate old emitter
};
const onEnd = () => {
if (stream.destroyed) return;
stream.push(null); // pushing null, indicating EOF
};
const onError = (err) => {
stream.destroy(err);
};
stream._destroy = (err, cb) => {
this._connection && this._connection.resume();
this.removeListener('result', onResult);
this.removeListener('fields', onFields);
this.removeListener('end', onEnd);
this.removeListener('error', onError);
cb(err); // Pass on any errors
};
this.on('result', onResult);
this.on('fields', onFields);
this.on('end', onEnd);
this.on('error', onError);
return stream;
}
_setTimeout() {
if (this.timeout) {
const timeoutHandler = this._handleTimeoutError.bind(this);
this.queryTimeout = Timers.setTimeout(timeoutHandler, this.timeout);
}
}
_handleTimeoutError() {
if (this.queryTimeout) {
Timers.clearTimeout(this.queryTimeout);
this.queryTimeout = null;
}
const err = new Error('Query inactivity timeout');
err.errorno = 'PROTOCOL_SEQUENCE_TIMEOUT';
err.code = 'PROTOCOL_SEQUENCE_TIMEOUT';
err.syscall = 'query';
if (this.onResult) {
this.onResult(err);
} else {
this.emit('error', err);
}
}
}
Query.prototype.catch = Query.prototype.then;
module.exports = Query;
+29
View File
@@ -0,0 +1,29 @@
'use strict';
const Command = require('./command.js');
const CommandCode = require('../constants/commands.js');
const Packet = require('../packets/packet.js');
class Quit extends Command {
constructor(callback) {
super();
this.onResult = callback;
}
start(packet, connection) {
connection._closing = true;
const quit = new Packet(
0,
Buffer.from([1, 0, 0, 0, CommandCode.QUIT]),
0,
5
);
if (this.onResult) {
this.onResult();
}
connection.writePacket(quit);
return null;
}
}
module.exports = Quit;
+27
View File
@@ -0,0 +1,27 @@
'use strict';
const Command = require('./command');
const Packets = require('../packets');
class RegisterSlave extends Command {
constructor(opts, callback) {
super();
this.onResult = callback;
this.opts = opts;
}
start(packet, connection) {
const newPacket = new Packets.RegisterSlave(this.opts);
connection.writePacket(newPacket.toPacket(1));
return RegisterSlave.prototype.registerResponse;
}
registerResponse() {
if (this.onResult) {
process.nextTick(this.onResult.bind(this));
}
return null;
}
}
module.exports = RegisterSlave;
+29
View File
@@ -0,0 +1,29 @@
'use strict';
const Command = require('./command');
const Packets = require('../packets/index.js');
class ResetConnection extends Command {
constructor(callback) {
super();
this.onResult = callback;
}
start(packet, connection) {
const req = new Packets.ResetConnection();
connection.writePacket(req.toPacket());
return ResetConnection.prototype.resetConnectionResponse;
}
resetConnectionResponse(packet, connection) {
if (connection._statements) {
connection._statements.clear();
}
if (this.onResult) {
process.nextTick(this.onResult.bind(this, null));
}
return null;
}
}
module.exports = ResetConnection;
+205
View File
@@ -0,0 +1,205 @@
'use strict';
const CommandCode = require('../constants/commands.js');
const Errors = require('../constants/errors.js');
const Command = require('./command.js');
const Packets = require('../packets/index.js');
class ServerHandshake extends Command {
constructor(args) {
super();
this.args = args;
/*
this.protocolVersion = args.protocolVersion || 10;
this.serverVersion = args.serverVersion;
this.connectionId = args.connectionId,
this.statusFlags = args.statusFlags,
this.characterSet = args.characterSet,
this.capabilityFlags = args.capabilityFlags || 512;
*/
}
start(packet, connection) {
const serverHelloPacket = new Packets.Handshake(this.args);
this.serverHello = serverHelloPacket;
serverHelloPacket.setScrambleData((err) => {
if (err) {
connection.emit('error', new Error('Error generating random bytes'));
return;
}
connection.writePacket(serverHelloPacket.toPacket(0));
});
return ServerHandshake.prototype.readClientReply;
}
readClientReply(packet, connection) {
// check auth here
const clientHelloReply = Packets.HandshakeResponse.fromPacket(
packet,
this.args.capabilityFlags
);
// TODO check we don't have something similar already
connection.clientHelloReply = clientHelloReply;
if (this.args.authCallback) {
this.args.authCallback(
{
user: clientHelloReply.user,
database: clientHelloReply.database,
address: connection.stream.remoteAddress,
authPluginData1: this.serverHello.authPluginData1,
authPluginData2: this.serverHello.authPluginData2,
authToken: clientHelloReply.authToken,
},
(err, mysqlError) => {
// if (err)
if (!mysqlError) {
connection.writeOk();
} else {
// TODO create constants / errorToCode
// 1045 = ER_ACCESS_DENIED_ERROR
connection.writeError({
message: mysqlError.message || '',
code: mysqlError.code || 1045,
});
connection.close();
}
}
);
} else {
connection.writeOk();
}
return ServerHandshake.prototype.dispatchCommands;
}
_isStatement(query, name) {
const firstWord = query.split(' ')[0].toUpperCase();
return firstWord === name;
}
dispatchCommands(packet, connection) {
// command from client to server
let knownCommand = true;
const encoding = connection.clientHelloReply.encoding;
const commandCode = packet.readInt8();
switch (commandCode) {
case CommandCode.STMT_PREPARE:
if (connection.listeners('stmt_prepare').length) {
const query = packet.readString(undefined, encoding);
connection.emit('stmt_prepare', query);
} else {
connection.writeError({
code: Errors.HA_ERR_INTERNAL_ERROR,
message: 'No query handler for prepared statements.',
});
}
break;
case CommandCode.STMT_EXECUTE:
if (connection.listeners('stmt_execute').length) {
const { stmtId, flags, iterationCount, values } =
Packets.Execute.fromPacket(packet, encoding);
connection.emit(
'stmt_execute',
stmtId,
flags,
iterationCount,
values
);
} else {
connection.writeError({
code: Errors.HA_ERR_INTERNAL_ERROR,
message: 'No query handler for execute statements.',
});
}
break;
case CommandCode.QUIT:
if (connection.listeners('quit').length) {
connection.emit('quit');
} else {
connection.stream.end();
}
break;
case CommandCode.INIT_DB:
if (connection.listeners('init_db').length) {
const schemaName = packet.readString(undefined, encoding);
connection.emit('init_db', schemaName);
} else {
connection.writeOk();
}
break;
case CommandCode.QUERY:
if (connection.listeners('query').length) {
const query = packet.readString(undefined, encoding);
if (
this._isStatement(query, 'PREPARE') ||
this._isStatement(query, 'SET')
) {
connection.emit('stmt_prepare', query);
} else if (this._isStatement(query, 'EXECUTE')) {
connection.emit('stmt_execute', null, null, null, null, query);
} else connection.emit('query', query);
} else {
connection.writeError({
code: Errors.HA_ERR_INTERNAL_ERROR,
message: 'No query handler',
});
}
break;
case CommandCode.FIELD_LIST:
if (connection.listeners('field_list').length) {
const table = packet.readNullTerminatedString(encoding);
const fields = packet.readString(undefined, encoding);
connection.emit('field_list', table, fields);
} else {
connection.writeError({
code: Errors.ER_WARN_DEPRECATED_SYNTAX,
message:
'As of MySQL 5.7.11, COM_FIELD_LIST is deprecated and will be removed in a future version of MySQL.',
});
}
break;
case CommandCode.PING:
if (connection.listeners('ping').length) {
connection.emit('ping');
} else {
connection.writeOk();
}
break;
default:
knownCommand = false;
}
if (connection.listeners('packet').length) {
connection.emit('packet', packet.clone(), knownCommand, commandCode);
} else if (!knownCommand) {
console.log('Unknown command:', commandCode);
}
return ServerHandshake.prototype.dispatchCommands;
}
}
module.exports = ServerHandshake;
// TODO: implement server-side 4.1 authentication
/*
4.1 authentication: (http://bazaar.launchpad.net/~mysql/mysql-server/5.5/view/head:/sql/password.c)
SERVER: public_seed=create_random_string()
send(public_seed)
CLIENT: recv(public_seed)
hash_stage1=sha1("password")
hash_stage2=sha1(hash_stage1)
reply=xor(hash_stage1, sha1(public_seed,hash_stage2)
// this three steps are done in scramble()
send(reply)
SERVER: recv(reply)
hash_stage1=xor(reply, sha1(public_seed,hash_stage2))
candidate_hash2=sha1(hash_stage1)
check(candidate_hash2==hash_stage2)
server stores sha1(sha1(password)) ( hash_stag2)
*/
+153
View File
@@ -0,0 +1,153 @@
'use strict';
// connection mixins
// implementation of http://dev.mysql.com/doc/internals/en/compression.html
const zlib = require('zlib');
const PacketParser = require('./packet_parser.js');
class Queue {
constructor() {
this._queue = [];
this._running = false;
}
push(fn) {
this._queue.push(fn);
if (!this._running) {
this._running = true;
process.nextTick(() => this._next());
}
}
_next() {
const task = this._queue.shift();
if (!task) {
this._running = false;
return;
}
task({
done: () => process.nextTick(() => this._next()),
});
}
}
function handleCompressedPacket(packet) {
// eslint-disable-next-line consistent-this, no-invalid-this
const connection = this;
const deflatedLength = packet.readInt24();
const body = packet.readBuffer();
if (deflatedLength !== 0) {
connection.inflateQueue.push((task) => {
zlib.inflate(body, (err, data) => {
if (err) {
connection._handleNetworkError(err);
return;
}
connection._bumpCompressedSequenceId(packet.numPackets);
connection._inflatedPacketsParser.execute(data);
task.done();
});
});
} else {
connection.inflateQueue.push((task) => {
connection._bumpCompressedSequenceId(packet.numPackets);
connection._inflatedPacketsParser.execute(body);
task.done();
});
}
}
function writeCompressed(buffer) {
// http://dev.mysql.com/doc/internals/en/example-several-mysql-packets.html
// note: sending a MySQL Packet of the size 2^245 to 2^241 via compression
// leads to at least one extra compressed packet.
// (this is because "length of the packet before compression" need to fit
// into 3 byte unsigned int. "length of the packet before compression" includes
// 4 byte packet header, hence 2^245)
const MAX_COMPRESSED_LENGTH = 16777210;
let start;
if (buffer.length > MAX_COMPRESSED_LENGTH) {
for (start = 0; start < buffer.length; start += MAX_COMPRESSED_LENGTH) {
writeCompressed.call(
// eslint-disable-next-line no-invalid-this
this,
buffer.slice(start, start + MAX_COMPRESSED_LENGTH)
);
}
return;
}
// eslint-disable-next-line no-invalid-this, consistent-this
const connection = this;
let packetLen = buffer.length;
const compressHeader = Buffer.allocUnsafe(7);
// seqqueue is used here because zlib async execution is routed via thread pool
// internally and when we have multiple compressed packets arriving we need
// to assemble uncompressed result sequentially
(function (seqId) {
connection.deflateQueue.push((task) => {
zlib.deflate(buffer, (err, compressed) => {
if (err) {
connection._handleFatalError(err);
return;
}
let compressedLength = compressed.length;
if (compressedLength < packetLen) {
compressHeader.writeUInt8(compressedLength & 0xff, 0);
compressHeader.writeUInt16LE(compressedLength >> 8, 1);
compressHeader.writeUInt8(seqId, 3);
compressHeader.writeUInt8(packetLen & 0xff, 4);
compressHeader.writeUInt16LE(packetLen >> 8, 5);
connection.writeUncompressed(compressHeader);
connection.writeUncompressed(compressed);
} else {
// http://dev.mysql.com/doc/internals/en/uncompressed-payload.html
// To send an uncompressed payload:
// - set length of payload before compression to 0
// - the compressed payload contains the uncompressed payload instead.
compressedLength = packetLen;
packetLen = 0;
compressHeader.writeUInt8(compressedLength & 0xff, 0);
compressHeader.writeUInt16LE(compressedLength >> 8, 1);
compressHeader.writeUInt8(seqId, 3);
compressHeader.writeUInt8(packetLen & 0xff, 4);
compressHeader.writeUInt16LE(packetLen >> 8, 5);
connection.writeUncompressed(compressHeader);
connection.writeUncompressed(buffer);
}
task.done();
});
});
})(connection.compressedSequenceId);
connection._bumpCompressedSequenceId(1);
}
function enableCompression(connection) {
connection._lastWrittenPacketId = 0;
connection._lastReceivedPacketId = 0;
connection._handleCompressedPacket = handleCompressedPacket;
connection._inflatedPacketsParser = new PacketParser((p) => {
connection.handlePacket(p);
}, 4);
connection._inflatedPacketsParser._lastPacket = 0;
connection.packetParser = new PacketParser((packet) => {
connection._handleCompressedPacket(packet);
}, 7);
connection.writeUncompressed = connection.write;
connection.write = writeCompressed;
connection.inflateQueue = new Queue();
connection.deflateQueue = new Queue();
}
module.exports = {
enableCompression: enableCompression,
Queue: Queue,
};
+12
View File
@@ -0,0 +1,12 @@
'use strict';
const BaseConnection = require('./base/connection.js');
class Connection extends BaseConnection {
promise(promiseImpl) {
const PromiseConnection = require('./promise/connection.js');
return new PromiseConnection(this, promiseImpl);
}
}
module.exports = Connection;
+300
View File
@@ -0,0 +1,300 @@
// This file was modified by Oracle on September 21, 2021.
// New connection options for additional authentication factors were
// introduced.
// Multi-factor authentication capability is now enabled if one of these
// options is used.
// Modifications copyright (c) 2021, Oracle and/or its affiliates.
'use strict';
const { URL } = require('url');
const ClientConstants = require('./constants/client');
const Charsets = require('./constants/charsets');
const { version } = require('../package.json');
let SSLProfiles = null;
const validOptions = {
authPlugins: 1,
authSwitchHandler: 1,
bigNumberStrings: 1,
charset: 1,
charsetNumber: 1,
compress: 1,
connectAttributes: 1,
connectTimeout: 1,
database: 1,
dateStrings: 1,
debug: 1,
decimalNumbers: 1,
enableKeepAlive: 1,
flags: 1,
host: 1,
insecureAuth: 1,
infileStreamFactory: 1,
isServer: 1,
keepAliveInitialDelay: 1,
localAddress: 1,
maxPreparedStatements: 1,
multipleStatements: 1,
namedPlaceholders: 1,
nestTables: 1,
password: 1,
// with multi-factor authentication, the main password (used for the first
// authentication factor) can be provided via password1
password1: 1,
password2: 1,
password3: 1,
passwordSha1: 1,
pool: 1,
port: 1,
queryFormat: 1,
rowsAsArray: 1,
socketPath: 1,
ssl: 1,
stream: 1,
stringifyObjects: 1,
supportBigNumbers: 1,
timezone: 1,
trace: 1,
typeCast: 1,
uri: 1,
user: 1,
disableEval: 1,
enableCleartextPlugin: 1,
// These options are used for Pool
connectionLimit: 1,
maxIdle: 1,
idleTimeout: 1,
Promise: 1,
queueLimit: 1,
resetOnRelease: 1,
waitForConnections: 1,
jsonStrings: 1,
gracefulEnd: 1,
};
class ConnectionConfig {
constructor(options) {
if (typeof options === 'string') {
options = ConnectionConfig.parseUrl(options);
} else if (options && options.uri) {
const uriOptions = ConnectionConfig.parseUrl(options.uri);
for (const key in uriOptions) {
if (!Object.prototype.hasOwnProperty.call(uriOptions, key)) continue;
if (options[key]) continue;
options[key] = uriOptions[key];
}
}
for (const key in options) {
if (!Object.prototype.hasOwnProperty.call(options, key)) continue;
if (validOptions[key] !== 1) {
// REVIEW: Should this be emitted somehow?
console.error(
`Ignoring invalid configuration option passed to Connection: ${key}. This is currently a warning, but in future versions of MySQL2, an error will be thrown if you pass an invalid configuration option to a Connection`
);
}
}
this.isServer = options.isServer;
this.stream = options.stream;
this.host = options.host || 'localhost';
this.port =
(typeof options.port === 'string'
? parseInt(options.port, 10)
: options.port) || 3306;
this.localAddress = options.localAddress;
this.socketPath = options.socketPath;
this.user = options.user || undefined;
// for the purpose of multi-factor authentication, or not, the main
// password (used for the 1st authentication factor) can also be
// provided via the "password1" option
this.password = options.password || options.password1 || undefined;
this.password2 = options.password2 || undefined;
this.password3 = options.password3 || undefined;
this.passwordSha1 = options.passwordSha1 || undefined;
this.database = options.database;
this.connectTimeout = isNaN(options.connectTimeout)
? 10 * 1000
: options.connectTimeout;
this.insecureAuth = options.insecureAuth || false;
this.infileStreamFactory = options.infileStreamFactory || undefined;
this.supportBigNumbers = options.supportBigNumbers || false;
this.bigNumberStrings = options.bigNumberStrings || false;
this.decimalNumbers = options.decimalNumbers || false;
this.dateStrings = options.dateStrings || false;
this.debug = options.debug;
this.trace = options.trace !== false;
this.stringifyObjects = options.stringifyObjects || false;
this.enableKeepAlive = options.enableKeepAlive !== false;
this.keepAliveInitialDelay = options.keepAliveInitialDelay;
if (
options.timezone &&
!/^(?:local|Z|[ +-]\d\d:\d\d)$/.test(options.timezone)
) {
// strictly supports timezones specified by mysqljs/mysql:
// https://github.com/mysqljs/mysql#user-content-connection-options
console.error(
`Ignoring invalid timezone passed to Connection: ${options.timezone}. This is currently a warning, but in future versions of MySQL2, an error will be thrown if you pass an invalid configuration option to a Connection`
);
// SqlStrings falls back to UTC on invalid timezone
this.timezone = 'Z';
} else {
this.timezone = options.timezone || 'local';
}
this.queryFormat = options.queryFormat;
this.pool = options.pool || undefined;
this.ssl =
typeof options.ssl === 'string'
? ConnectionConfig.getSSLProfile(options.ssl)
: options.ssl || false;
this.multipleStatements = options.multipleStatements || false;
this.rowsAsArray = options.rowsAsArray || false;
this.namedPlaceholders = options.namedPlaceholders || false;
this.nestTables =
options.nestTables === undefined ? undefined : options.nestTables;
this.typeCast = options.typeCast === undefined ? true : options.typeCast;
this.disableEval = Boolean(options.disableEval);
this.enableCleartextPlugin = Boolean(options.enableCleartextPlugin);
if (this.timezone[0] === ' ') {
// "+" is a url encoded char for space so it
// gets translated to space when giving a
// connection string..
this.timezone = `+${this.timezone.slice(1)}`;
}
if (this.ssl) {
if (typeof this.ssl !== 'object') {
throw new TypeError(
`SSL profile must be an object, instead it's a ${typeof this.ssl}`
);
}
// Default rejectUnauthorized to true
this.ssl.rejectUnauthorized = this.ssl.rejectUnauthorized !== false;
}
this.maxPacketSize = 0;
this.charsetNumber = options.charset
? ConnectionConfig.getCharsetNumber(options.charset)
: options.charsetNumber || Charsets.UTF8MB4_UNICODE_CI;
this.compress = options.compress || false;
this.authPlugins = options.authPlugins;
this.authSwitchHandler = options.authSwitchHandler;
this.clientFlags = ConnectionConfig.mergeFlags(
ConnectionConfig.getDefaultFlags(options),
options.flags || ''
);
// Default connection attributes
// https://dev.mysql.com/doc/refman/8.0/en/performance-schema-connection-attribute-tables.html
const defaultConnectAttributes = {
_client_name: 'Node-MySQL-2',
_client_version: version,
};
this.connectAttributes = {
...defaultConnectAttributes,
...(options.connectAttributes || {}),
};
this.maxPreparedStatements = options.maxPreparedStatements || 16000;
this.jsonStrings = options.jsonStrings || false;
this.gracefulEnd = options.gracefulEnd || false;
}
static mergeFlags(default_flags, user_flags) {
let flags = 0x0,
i;
if (!Array.isArray(user_flags)) {
user_flags = String(user_flags || '')
.toUpperCase()
.split(/\s*,+\s*/);
}
// add default flags unless "blacklisted"
for (i in default_flags) {
if (user_flags.indexOf(`-${default_flags[i]}`) >= 0) {
continue;
}
flags |= ClientConstants[default_flags[i]] || 0x0;
}
// add user flags unless already already added
for (i in user_flags) {
if (user_flags[i][0] === '-') {
continue;
}
if (default_flags.indexOf(user_flags[i]) >= 0) {
continue;
}
flags |= ClientConstants[user_flags[i]] || 0x0;
}
return flags;
}
static getDefaultFlags(options) {
const defaultFlags = [
'LONG_PASSWORD',
'FOUND_ROWS',
'LONG_FLAG',
'CONNECT_WITH_DB',
'ODBC',
'LOCAL_FILES',
'IGNORE_SPACE',
'PROTOCOL_41',
'IGNORE_SIGPIPE',
'TRANSACTIONS',
'RESERVED',
'SECURE_CONNECTION',
'MULTI_RESULTS',
'TRANSACTIONS',
'SESSION_TRACK',
'CONNECT_ATTRS',
'CLIENT_QUERY_ATTRIBUTES',
];
if (options && options.multipleStatements) {
defaultFlags.push('MULTI_STATEMENTS');
}
defaultFlags.push('PLUGIN_AUTH');
defaultFlags.push('PLUGIN_AUTH_LENENC_CLIENT_DATA');
return defaultFlags;
}
static getCharsetNumber(charset) {
const num = Charsets[charset.toUpperCase()];
if (num === undefined) {
throw new TypeError(`Unknown charset '${charset}'`);
}
return num;
}
static getSSLProfile(name) {
if (!SSLProfiles) {
SSLProfiles = require('./constants/ssl_profiles.js');
}
const ssl = SSLProfiles[name];
if (ssl === undefined) {
throw new TypeError(`Unknown SSL profile '${name}'`);
}
return ssl;
}
static parseUrl(url) {
const parsedUrl = new URL(url);
const options = {
host: decodeURIComponent(parsedUrl.hostname),
port: parseInt(parsedUrl.port, 10),
database: decodeURIComponent(parsedUrl.pathname.slice(1)),
user: decodeURIComponent(parsedUrl.username),
password: decodeURIComponent(parsedUrl.password),
};
for (const [key, value] of parsedUrl.searchParams) {
if (key in options) {
continue;
}
try {
// Try to parse this as a JSON expression first
options[key] = JSON.parse(value);
} catch {
// Otherwise assume it is a plain string
options[key] = value;
}
}
return options;
}
}
module.exports = ConnectionConfig;
+317
View File
@@ -0,0 +1,317 @@
'use strict';
// see tools/generate-charset-mapping.js
// basicalliy result of "SHOW COLLATION" query
module.exports = [
'utf8',
'big5',
'latin2',
'dec8',
'cp850',
'latin1',
'hp8',
'koi8r',
'latin1',
'latin2',
'swe7',
'ascii',
'eucjp',
'sjis',
'cp1251',
'latin1',
'hebrew',
'utf8',
'tis620',
'euckr',
'latin7',
'latin2',
'koi8u',
'cp1251',
'gb2312',
'greek',
'cp1250',
'latin2',
'gbk',
'cp1257',
'latin5',
'latin1',
'armscii8',
'cesu8',
'cp1250',
'ucs2',
'cp866',
'keybcs2',
'macintosh',
'macroman',
'cp852',
'latin7',
'latin7',
'macintosh',
'cp1250',
'utf8',
'utf8',
'latin1',
'latin1',
'latin1',
'cp1251',
'cp1251',
'cp1251',
'macroman',
'utf16',
'utf16',
'utf16-le',
'cp1256',
'cp1257',
'cp1257',
'utf32',
'utf32',
'utf16-le',
'binary',
'armscii8',
'ascii',
'cp1250',
'cp1256',
'cp866',
'dec8',
'greek',
'hebrew',
'hp8',
'keybcs2',
'koi8r',
'koi8u',
'cesu8',
'latin2',
'latin5',
'latin7',
'cp850',
'cp852',
'swe7',
'cesu8',
'big5',
'euckr',
'gb2312',
'gbk',
'sjis',
'tis620',
'ucs2',
'eucjp',
'geostd8',
'geostd8',
'latin1',
'cp932',
'cp932',
'eucjpms',
'eucjpms',
'cp1250',
'utf16',
'utf16',
'utf16',
'utf16',
'utf16',
'utf16',
'utf16',
'utf16',
'utf16',
'utf16',
'utf16',
'utf16',
'utf16',
'utf16',
'utf16',
'utf16',
'utf16',
'utf16',
'utf16',
'utf16',
'utf16',
'utf16',
'utf16',
'utf16',
'utf16',
'utf8',
'utf8',
'utf8',
'ucs2',
'ucs2',
'ucs2',
'ucs2',
'ucs2',
'ucs2',
'ucs2',
'ucs2',
'ucs2',
'ucs2',
'ucs2',
'ucs2',
'ucs2',
'ucs2',
'ucs2',
'ucs2',
'ucs2',
'ucs2',
'ucs2',
'ucs2',
'ucs2',
'ucs2',
'ucs2',
'ucs2',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'ucs2',
'utf32',
'utf32',
'utf32',
'utf32',
'utf32',
'utf32',
'utf32',
'utf32',
'utf32',
'utf32',
'utf32',
'utf32',
'utf32',
'utf32',
'utf32',
'utf32',
'utf32',
'utf32',
'utf32',
'utf32',
'utf32',
'utf32',
'utf32',
'utf32',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'cesu8',
'cesu8',
'cesu8',
'cesu8',
'cesu8',
'cesu8',
'cesu8',
'cesu8',
'cesu8',
'cesu8',
'cesu8',
'cesu8',
'cesu8',
'cesu8',
'cesu8',
'cesu8',
'cesu8',
'cesu8',
'cesu8',
'cesu8',
'cesu8',
'cesu8',
'cesu8',
'cesu8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'cesu8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'gb18030',
'gb18030',
'gb18030',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
'utf8',
];
+317
View File
@@ -0,0 +1,317 @@
'use strict';
exports.BIG5_CHINESE_CI = 1;
exports.LATIN2_CZECH_CS = 2;
exports.DEC8_SWEDISH_CI = 3;
exports.CP850_GENERAL_CI = 4;
exports.LATIN1_GERMAN1_CI = 5;
exports.HP8_ENGLISH_CI = 6;
exports.KOI8R_GENERAL_CI = 7;
exports.LATIN1_SWEDISH_CI = 8;
exports.LATIN2_GENERAL_CI = 9;
exports.SWE7_SWEDISH_CI = 10;
exports.ASCII_GENERAL_CI = 11;
exports.UJIS_JAPANESE_CI = 12;
exports.SJIS_JAPANESE_CI = 13;
exports.CP1251_BULGARIAN_CI = 14;
exports.LATIN1_DANISH_CI = 15;
exports.HEBREW_GENERAL_CI = 16;
exports.TIS620_THAI_CI = 18;
exports.EUCKR_KOREAN_CI = 19;
exports.LATIN7_ESTONIAN_CS = 20;
exports.LATIN2_HUNGARIAN_CI = 21;
exports.KOI8U_GENERAL_CI = 22;
exports.CP1251_UKRAINIAN_CI = 23;
exports.GB2312_CHINESE_CI = 24;
exports.GREEK_GENERAL_CI = 25;
exports.CP1250_GENERAL_CI = 26;
exports.LATIN2_CROATIAN_CI = 27;
exports.GBK_CHINESE_CI = 28;
exports.CP1257_LITHUANIAN_CI = 29;
exports.LATIN5_TURKISH_CI = 30;
exports.LATIN1_GERMAN2_CI = 31;
exports.ARMSCII8_GENERAL_CI = 32;
exports.UTF8_GENERAL_CI = 33;
exports.CP1250_CZECH_CS = 34;
exports.UCS2_GENERAL_CI = 35;
exports.CP866_GENERAL_CI = 36;
exports.KEYBCS2_GENERAL_CI = 37;
exports.MACCE_GENERAL_CI = 38;
exports.MACROMAN_GENERAL_CI = 39;
exports.CP852_GENERAL_CI = 40;
exports.LATIN7_GENERAL_CI = 41;
exports.LATIN7_GENERAL_CS = 42;
exports.MACCE_BIN = 43;
exports.CP1250_CROATIAN_CI = 44;
exports.UTF8MB4_GENERAL_CI = 45;
exports.UTF8MB4_BIN = 46;
exports.LATIN1_BIN = 47;
exports.LATIN1_GENERAL_CI = 48;
exports.LATIN1_GENERAL_CS = 49;
exports.CP1251_BIN = 50;
exports.CP1251_GENERAL_CI = 51;
exports.CP1251_GENERAL_CS = 52;
exports.MACROMAN_BIN = 53;
exports.UTF16_GENERAL_CI = 54;
exports.UTF16_BIN = 55;
exports.UTF16LE_GENERAL_CI = 56;
exports.CP1256_GENERAL_CI = 57;
exports.CP1257_BIN = 58;
exports.CP1257_GENERAL_CI = 59;
exports.UTF32_GENERAL_CI = 60;
exports.UTF32_BIN = 61;
exports.UTF16LE_BIN = 62;
exports.BINARY = 63;
exports.ARMSCII8_BIN = 64;
exports.ASCII_BIN = 65;
exports.CP1250_BIN = 66;
exports.CP1256_BIN = 67;
exports.CP866_BIN = 68;
exports.DEC8_BIN = 69;
exports.GREEK_BIN = 70;
exports.HEBREW_BIN = 71;
exports.HP8_BIN = 72;
exports.KEYBCS2_BIN = 73;
exports.KOI8R_BIN = 74;
exports.KOI8U_BIN = 75;
exports.UTF8_TOLOWER_CI = 76;
exports.LATIN2_BIN = 77;
exports.LATIN5_BIN = 78;
exports.LATIN7_BIN = 79;
exports.CP850_BIN = 80;
exports.CP852_BIN = 81;
exports.SWE7_BIN = 82;
exports.UTF8_BIN = 83;
exports.BIG5_BIN = 84;
exports.EUCKR_BIN = 85;
exports.GB2312_BIN = 86;
exports.GBK_BIN = 87;
exports.SJIS_BIN = 88;
exports.TIS620_BIN = 89;
exports.UCS2_BIN = 90;
exports.UJIS_BIN = 91;
exports.GEOSTD8_GENERAL_CI = 92;
exports.GEOSTD8_BIN = 93;
exports.LATIN1_SPANISH_CI = 94;
exports.CP932_JAPANESE_CI = 95;
exports.CP932_BIN = 96;
exports.EUCJPMS_JAPANESE_CI = 97;
exports.EUCJPMS_BIN = 98;
exports.CP1250_POLISH_CI = 99;
exports.UTF16_UNICODE_CI = 101;
exports.UTF16_ICELANDIC_CI = 102;
exports.UTF16_LATVIAN_CI = 103;
exports.UTF16_ROMANIAN_CI = 104;
exports.UTF16_SLOVENIAN_CI = 105;
exports.UTF16_POLISH_CI = 106;
exports.UTF16_ESTONIAN_CI = 107;
exports.UTF16_SPANISH_CI = 108;
exports.UTF16_SWEDISH_CI = 109;
exports.UTF16_TURKISH_CI = 110;
exports.UTF16_CZECH_CI = 111;
exports.UTF16_DANISH_CI = 112;
exports.UTF16_LITHUANIAN_CI = 113;
exports.UTF16_SLOVAK_CI = 114;
exports.UTF16_SPANISH2_CI = 115;
exports.UTF16_ROMAN_CI = 116;
exports.UTF16_PERSIAN_CI = 117;
exports.UTF16_ESPERANTO_CI = 118;
exports.UTF16_HUNGARIAN_CI = 119;
exports.UTF16_SINHALA_CI = 120;
exports.UTF16_GERMAN2_CI = 121;
exports.UTF16_CROATIAN_CI = 122;
exports.UTF16_UNICODE_520_CI = 123;
exports.UTF16_VIETNAMESE_CI = 124;
exports.UCS2_UNICODE_CI = 128;
exports.UCS2_ICELANDIC_CI = 129;
exports.UCS2_LATVIAN_CI = 130;
exports.UCS2_ROMANIAN_CI = 131;
exports.UCS2_SLOVENIAN_CI = 132;
exports.UCS2_POLISH_CI = 133;
exports.UCS2_ESTONIAN_CI = 134;
exports.UCS2_SPANISH_CI = 135;
exports.UCS2_SWEDISH_CI = 136;
exports.UCS2_TURKISH_CI = 137;
exports.UCS2_CZECH_CI = 138;
exports.UCS2_DANISH_CI = 139;
exports.UCS2_LITHUANIAN_CI = 140;
exports.UCS2_SLOVAK_CI = 141;
exports.UCS2_SPANISH2_CI = 142;
exports.UCS2_ROMAN_CI = 143;
exports.UCS2_PERSIAN_CI = 144;
exports.UCS2_ESPERANTO_CI = 145;
exports.UCS2_HUNGARIAN_CI = 146;
exports.UCS2_SINHALA_CI = 147;
exports.UCS2_GERMAN2_CI = 148;
exports.UCS2_CROATIAN_CI = 149;
exports.UCS2_UNICODE_520_CI = 150;
exports.UCS2_VIETNAMESE_CI = 151;
exports.UCS2_GENERAL_MYSQL500_CI = 159;
exports.UTF32_UNICODE_CI = 160;
exports.UTF32_ICELANDIC_CI = 161;
exports.UTF32_LATVIAN_CI = 162;
exports.UTF32_ROMANIAN_CI = 163;
exports.UTF32_SLOVENIAN_CI = 164;
exports.UTF32_POLISH_CI = 165;
exports.UTF32_ESTONIAN_CI = 166;
exports.UTF32_SPANISH_CI = 167;
exports.UTF32_SWEDISH_CI = 168;
exports.UTF32_TURKISH_CI = 169;
exports.UTF32_CZECH_CI = 170;
exports.UTF32_DANISH_CI = 171;
exports.UTF32_LITHUANIAN_CI = 172;
exports.UTF32_SLOVAK_CI = 173;
exports.UTF32_SPANISH2_CI = 174;
exports.UTF32_ROMAN_CI = 175;
exports.UTF32_PERSIAN_CI = 176;
exports.UTF32_ESPERANTO_CI = 177;
exports.UTF32_HUNGARIAN_CI = 178;
exports.UTF32_SINHALA_CI = 179;
exports.UTF32_GERMAN2_CI = 180;
exports.UTF32_CROATIAN_CI = 181;
exports.UTF32_UNICODE_520_CI = 182;
exports.UTF32_VIETNAMESE_CI = 183;
exports.UTF8_UNICODE_CI = 192;
exports.UTF8_ICELANDIC_CI = 193;
exports.UTF8_LATVIAN_CI = 194;
exports.UTF8_ROMANIAN_CI = 195;
exports.UTF8_SLOVENIAN_CI = 196;
exports.UTF8_POLISH_CI = 197;
exports.UTF8_ESTONIAN_CI = 198;
exports.UTF8_SPANISH_CI = 199;
exports.UTF8_SWEDISH_CI = 200;
exports.UTF8_TURKISH_CI = 201;
exports.UTF8_CZECH_CI = 202;
exports.UTF8_DANISH_CI = 203;
exports.UTF8_LITHUANIAN_CI = 204;
exports.UTF8_SLOVAK_CI = 205;
exports.UTF8_SPANISH2_CI = 206;
exports.UTF8_ROMAN_CI = 207;
exports.UTF8_PERSIAN_CI = 208;
exports.UTF8_ESPERANTO_CI = 209;
exports.UTF8_HUNGARIAN_CI = 210;
exports.UTF8_SINHALA_CI = 211;
exports.UTF8_GERMAN2_CI = 212;
exports.UTF8_CROATIAN_CI = 213;
exports.UTF8_UNICODE_520_CI = 214;
exports.UTF8_VIETNAMESE_CI = 215;
exports.UTF8_GENERAL_MYSQL500_CI = 223;
exports.UTF8MB4_UNICODE_CI = 224;
exports.UTF8MB4_ICELANDIC_CI = 225;
exports.UTF8MB4_LATVIAN_CI = 226;
exports.UTF8MB4_ROMANIAN_CI = 227;
exports.UTF8MB4_SLOVENIAN_CI = 228;
exports.UTF8MB4_POLISH_CI = 229;
exports.UTF8MB4_ESTONIAN_CI = 230;
exports.UTF8MB4_SPANISH_CI = 231;
exports.UTF8MB4_SWEDISH_CI = 232;
exports.UTF8MB4_TURKISH_CI = 233;
exports.UTF8MB4_CZECH_CI = 234;
exports.UTF8MB4_DANISH_CI = 235;
exports.UTF8MB4_LITHUANIAN_CI = 236;
exports.UTF8MB4_SLOVAK_CI = 237;
exports.UTF8MB4_SPANISH2_CI = 238;
exports.UTF8MB4_ROMAN_CI = 239;
exports.UTF8MB4_PERSIAN_CI = 240;
exports.UTF8MB4_ESPERANTO_CI = 241;
exports.UTF8MB4_HUNGARIAN_CI = 242;
exports.UTF8MB4_SINHALA_CI = 243;
exports.UTF8MB4_GERMAN2_CI = 244;
exports.UTF8MB4_CROATIAN_CI = 245;
exports.UTF8MB4_UNICODE_520_CI = 246;
exports.UTF8MB4_VIETNAMESE_CI = 247;
exports.GB18030_CHINESE_CI = 248;
exports.GB18030_BIN = 249;
exports.GB18030_UNICODE_520_CI = 250;
exports.UTF8_GENERAL50_CI = 253; // deprecated
exports.UTF8MB4_0900_AI_CI = 255;
exports.UTF8MB4_DE_PB_0900_AI_CI = 256;
exports.UTF8MB4_IS_0900_AI_CI = 257;
exports.UTF8MB4_LV_0900_AI_CI = 258;
exports.UTF8MB4_RO_0900_AI_CI = 259;
exports.UTF8MB4_SL_0900_AI_CI = 260;
exports.UTF8MB4_PL_0900_AI_CI = 261;
exports.UTF8MB4_ET_0900_AI_CI = 262;
exports.UTF8MB4_ES_0900_AI_CI = 263;
exports.UTF8MB4_SV_0900_AI_CI = 264;
exports.UTF8MB4_TR_0900_AI_CI = 265;
exports.UTF8MB4_CS_0900_AI_CI = 266;
exports.UTF8MB4_DA_0900_AI_CI = 267;
exports.UTF8MB4_LT_0900_AI_CI = 268;
exports.UTF8MB4_SK_0900_AI_CI = 269;
exports.UTF8MB4_ES_TRAD_0900_AI_CI = 270;
exports.UTF8MB4_LA_0900_AI_CI = 271;
exports.UTF8MB4_EO_0900_AI_CI = 273;
exports.UTF8MB4_HU_0900_AI_CI = 274;
exports.UTF8MB4_HR_0900_AI_CI = 275;
exports.UTF8MB4_VI_0900_AI_CI = 277;
exports.UTF8MB4_0900_AS_CS = 278;
exports.UTF8MB4_DE_PB_0900_AS_CS = 279;
exports.UTF8MB4_IS_0900_AS_CS = 280;
exports.UTF8MB4_LV_0900_AS_CS = 281;
exports.UTF8MB4_RO_0900_AS_CS = 282;
exports.UTF8MB4_SL_0900_AS_CS = 283;
exports.UTF8MB4_PL_0900_AS_CS = 284;
exports.UTF8MB4_ET_0900_AS_CS = 285;
exports.UTF8MB4_ES_0900_AS_CS = 286;
exports.UTF8MB4_SV_0900_AS_CS = 287;
exports.UTF8MB4_TR_0900_AS_CS = 288;
exports.UTF8MB4_CS_0900_AS_CS = 289;
exports.UTF8MB4_DA_0900_AS_CS = 290;
exports.UTF8MB4_LT_0900_AS_CS = 291;
exports.UTF8MB4_SK_0900_AS_CS = 292;
exports.UTF8MB4_ES_TRAD_0900_AS_CS = 293;
exports.UTF8MB4_LA_0900_AS_CS = 294;
exports.UTF8MB4_EO_0900_AS_CS = 296;
exports.UTF8MB4_HU_0900_AS_CS = 297;
exports.UTF8MB4_HR_0900_AS_CS = 298;
exports.UTF8MB4_VI_0900_AS_CS = 300;
exports.UTF8MB4_JA_0900_AS_CS = 303;
exports.UTF8MB4_JA_0900_AS_CS_KS = 304;
exports.UTF8MB4_0900_AS_CI = 305;
exports.UTF8MB4_RU_0900_AI_CI = 306;
exports.UTF8MB4_RU_0900_AS_CS = 307;
exports.UTF8MB4_ZH_0900_AS_CS = 308;
exports.UTF8MB4_0900_BIN = 309;
// short aliases
exports.BIG5 = exports.BIG5_CHINESE_CI;
exports.DEC8 = exports.DEC8_SWEDISH_CI;
exports.CP850 = exports.CP850_GENERAL_CI;
exports.HP8 = exports.HP8_ENGLISH_CI;
exports.KOI8R = exports.KOI8R_GENERAL_CI;
exports.LATIN1 = exports.LATIN1_SWEDISH_CI;
exports.LATIN2 = exports.LATIN2_GENERAL_CI;
exports.SWE7 = exports.SWE7_SWEDISH_CI;
exports.ASCII = exports.ASCII_GENERAL_CI;
exports.UJIS = exports.UJIS_JAPANESE_CI;
exports.SJIS = exports.SJIS_JAPANESE_CI;
exports.HEBREW = exports.HEBREW_GENERAL_CI;
exports.TIS620 = exports.TIS620_THAI_CI;
exports.EUCKR = exports.EUCKR_KOREAN_CI;
exports.KOI8U = exports.KOI8U_GENERAL_CI;
exports.GB2312 = exports.GB2312_CHINESE_CI;
exports.GREEK = exports.GREEK_GENERAL_CI;
exports.CP1250 = exports.CP1250_GENERAL_CI;
exports.GBK = exports.GBK_CHINESE_CI;
exports.LATIN5 = exports.LATIN5_TURKISH_CI;
exports.ARMSCII8 = exports.ARMSCII8_GENERAL_CI;
exports.UTF8 = exports.UTF8_GENERAL_CI;
exports.UCS2 = exports.UCS2_GENERAL_CI;
exports.CP866 = exports.CP866_GENERAL_CI;
exports.KEYBCS2 = exports.KEYBCS2_GENERAL_CI;
exports.MACCE = exports.MACCE_GENERAL_CI;
exports.MACROMAN = exports.MACROMAN_GENERAL_CI;
exports.CP852 = exports.CP852_GENERAL_CI;
exports.LATIN7 = exports.LATIN7_GENERAL_CI;
exports.UTF8MB4 = exports.UTF8MB4_GENERAL_CI;
exports.CP1251 = exports.CP1251_GENERAL_CI;
exports.UTF16 = exports.UTF16_GENERAL_CI;
exports.UTF16LE = exports.UTF16LE_GENERAL_CI;
exports.CP1256 = exports.CP1256_GENERAL_CI;
exports.CP1257 = exports.CP1257_GENERAL_CI;
exports.UTF32 = exports.UTF32_GENERAL_CI;
exports.CP932 = exports.CP932_JAPANESE_CI;
exports.EUCJPMS = exports.EUCJPMS_JAPANESE_CI;
exports.GB18030 = exports.GB18030_CHINESE_CI;
exports.GEOSTD8 = exports.GEOSTD8_GENERAL_CI;
+39
View File
@@ -0,0 +1,39 @@
// This file was modified by Oracle on September 21, 2021.
// New capability for multi-factor authentication based on mandatory session
// trackers, that are signaled with an extra single-byte prefix on new
// versions of the MySQL server.
// Modifications copyright (c) 2021, Oracle and/or its affiliates.
'use strict';
// Manually extracted from mysql-5.5.23/include/mysql_com.h
exports.LONG_PASSWORD = 0x00000001; /* new more secure passwords */
exports.FOUND_ROWS = 0x00000002; /* found instead of affected rows */
exports.LONG_FLAG = 0x00000004; /* get all column flags */
exports.CONNECT_WITH_DB = 0x00000008; /* one can specify db on connect */
exports.NO_SCHEMA = 0x00000010; /* don't allow database.table.column */
exports.COMPRESS = 0x00000020; /* can use compression protocol */
exports.ODBC = 0x00000040; /* odbc client */
exports.LOCAL_FILES = 0x00000080; /* can use LOAD DATA LOCAL */
exports.IGNORE_SPACE = 0x00000100; /* ignore spaces before '' */
exports.PROTOCOL_41 = 0x00000200; /* new 4.1 protocol */
exports.INTERACTIVE = 0x00000400; /* this is an interactive client */
exports.SSL = 0x00000800; /* switch to ssl after handshake */
exports.IGNORE_SIGPIPE = 0x00001000; /* IGNORE sigpipes */
exports.TRANSACTIONS = 0x00002000; /* client knows about transactions */
exports.RESERVED = 0x00004000; /* old flag for 4.1 protocol */
exports.SECURE_CONNECTION = 0x00008000; /* new 4.1 authentication */
exports.MULTI_STATEMENTS = 0x00010000; /* enable/disable multi-stmt support */
exports.MULTI_RESULTS = 0x00020000; /* enable/disable multi-results */
exports.PS_MULTI_RESULTS = 0x00040000; /* multi-results in ps-protocol */
exports.PLUGIN_AUTH = 0x00080000; /* client supports plugin authentication */
exports.CONNECT_ATTRS = 0x00100000; /* permits connection attributes */
exports.PLUGIN_AUTH_LENENC_CLIENT_DATA = 0x00200000; /* Understands length-encoded integer for auth response data in Protocol::HandshakeResponse41. */
exports.CAN_HANDLE_EXPIRED_PASSWORDS = 0x00400000; /* Announces support for expired password extension. */
exports.SESSION_TRACK = 0x00800000; /* Can set SERVER_SESSION_STATE_CHANGED in the Status Flags and send session-state change data after a OK packet. */
exports.CLIENT_QUERY_ATTRIBUTES = 0x08000000; /* support query attributes in COM_QUERY and COM_STMT_EXECUTE */
exports.SSL_VERIFY_SERVER_CERT = 0x40000000;
exports.REMEMBER_OPTIONS = 0x80000000;
exports.MULTI_FACTOR_AUTHENTICATION = 0x10000000; /* multi-factor authentication */
+37
View File
@@ -0,0 +1,37 @@
'use strict';
module.exports = {
SLEEP: 0x00, // deprecated
QUIT: 0x01,
INIT_DB: 0x02,
QUERY: 0x03,
FIELD_LIST: 0x04,
CREATE_DB: 0x05,
DROP_DB: 0x06,
REFRESH: 0x07,
SHUTDOWN: 0x08,
STATISTICS: 0x09,
PROCESS_INFO: 0x0a, // deprecated
CONNECT: 0x0b, // deprecated
PROCESS_KILL: 0x0c,
DEBUG: 0x0d,
PING: 0x0e,
TIME: 0x0f, // deprecated
DELAYED_INSERT: 0x10, // deprecated
CHANGE_USER: 0x11,
BINLOG_DUMP: 0x12,
TABLE_DUMP: 0x13,
CONNECT_OUT: 0x14,
REGISTER_SLAVE: 0x15,
STMT_PREPARE: 0x16,
STMT_EXECUTE: 0x17,
STMT_SEND_LONG_DATA: 0x18,
STMT_CLOSE: 0x19,
STMT_RESET: 0x1a,
SET_OPTION: 0x1b,
STMT_FETCH: 0x1c,
DAEMON: 0x1d, // deprecated
BINLOG_DUMP_GTID: 0x1e,
RESET_CONNECTION: 0x1f, // introduced in 5.7.3
UNKNOWN: 0xff, // bad!
};
+9
View File
@@ -0,0 +1,9 @@
'use strict';
module.exports = {
NO_CURSOR: 0,
READ_ONLY: 1,
FOR_UPDATE: 2,
SCROLLABLE: 3,
PARAMETER_COUNT_AVAILABLE: 8,
};
+50
View File
@@ -0,0 +1,50 @@
'use strict';
// inverse of charset_encodings
// given encoding, get matching mysql charset number
module.exports = {
big5: 1,
latin2: 2,
dec8: 3,
cp850: 4,
latin1: 5,
hp8: 6,
koi8r: 7,
swe7: 10,
ascii: 11,
eucjp: 12,
sjis: 13,
cp1251: 14,
hebrew: 16,
tis620: 18,
euckr: 19,
latin7: 20,
koi8u: 22,
gb2312: 24,
greek: 25,
cp1250: 26,
gbk: 28,
cp1257: 29,
latin5: 30,
armscii8: 32,
cesu8: 33,
ucs2: 35,
cp866: 36,
keybcs2: 37,
macintosh: 38,
macroman: 39,
cp852: 40,
utf8: 45,
utf8mb4: 45,
utf16: 54,
utf16le: 56,
cp1256: 57,
utf32: 60,
binary: 63,
geostd8: 92,
cp932: 95,
eucjpms: 97,
gb18030: 248,
utf8mb3: 192,
};
File diff suppressed because it is too large Load Diff
+20
View File
@@ -0,0 +1,20 @@
'use strict';
// Manually extracted from mysql-5.5.23/include/mysql_com.h
exports.NOT_NULL = 1; /* Field can't be NULL */
exports.PRI_KEY = 2; /* Field is part of a primary key */
exports.UNIQUE_KEY = 4; /* Field is part of a unique key */
exports.MULTIPLE_KEY = 8; /* Field is part of a key */
exports.BLOB = 16; /* Field is a blob */
exports.UNSIGNED = 32; /* Field is unsigned */
exports.ZEROFILL = 64; /* Field is zerofill */
exports.BINARY = 128; /* Field is binary */
/* The following are only sent to new clients */
exports.ENUM = 256; /* field is an enum */
exports.AUTO_INCREMENT = 512; /* field is a autoincrement field */
exports.TIMESTAMP = 1024; /* Field is a timestamp */
exports.SET = 2048; /* field is a set */
exports.NO_DEFAULT_VALUE = 4096; /* Field doesn't have default value */
exports.ON_UPDATE_NOW = 8192; /* Field is set to NOW on UPDATE */
exports.NUM = 32768; /* Field is num (for clients) */
+44
View File
@@ -0,0 +1,44 @@
'use strict';
// Manually extracted from mysql-5.5.23/include/mysql_com.h
/**
Is raised when a multi-statement transaction
has been started, either explicitly, by means
of BEGIN or COMMIT AND CHAIN, or
implicitly, by the first transactional
statement, when autocommit=off.
*/
exports.SERVER_STATUS_IN_TRANS = 1;
exports.SERVER_STATUS_AUTOCOMMIT = 2; /* Server in auto_commit mode */
exports.SERVER_MORE_RESULTS_EXISTS = 8; /* Multi query - next query exists */
exports.SERVER_QUERY_NO_GOOD_INDEX_USED = 16;
exports.SERVER_QUERY_NO_INDEX_USED = 32;
/**
The server was able to fulfill the clients request and opened a
read-only non-scrollable cursor for a query. This flag comes
in reply to COM_STMT_EXECUTE and COM_STMT_FETCH commands.
*/
exports.SERVER_STATUS_CURSOR_EXISTS = 64;
/**
This flag is sent when a read-only cursor is exhausted, in reply to
COM_STMT_FETCH command.
*/
exports.SERVER_STATUS_LAST_ROW_SENT = 128;
exports.SERVER_STATUS_DB_DROPPED = 256; /* A database was dropped */
exports.SERVER_STATUS_NO_BACKSLASH_ESCAPES = 512;
/**
Sent to the client if after a prepared statement reprepare
we discovered that the new statement returns a different
number of result set columns.
*/
exports.SERVER_STATUS_METADATA_CHANGED = 1024;
exports.SERVER_QUERY_WAS_SLOW = 2048;
/**
To mark ResultSet containing output parameter values.
*/
exports.SERVER_PS_OUT_PARAMS = 4096;
exports.SERVER_STATUS_IN_TRANS_READONLY = 0x2000; // in a read-only transaction
exports.SERVER_SESSION_STATE_CHANGED = 0x4000;
+11
View File
@@ -0,0 +1,11 @@
'use strict';
exports.SYSTEM_VARIABLES = 0;
exports.SCHEMA = 1;
exports.STATE_CHANGE = 2;
exports.STATE_GTIDS = 3;
exports.TRANSACTION_CHARACTERISTICS = 4;
exports.TRANSACTION_STATE = 5;
exports.FIRST_KEY = exports.SYSTEM_VARIABLES;
exports.LAST_KEY = exports.TRANSACTION_STATE;
+11
View File
@@ -0,0 +1,11 @@
'use strict';
const awsCaBundle = require('aws-ssl-profiles');
/**
* @deprecated
* Please, use [**aws-ssl-profiles**](https://github.com/mysqljs/aws-ssl-profiles).
*/
exports['Amazon RDS'] = {
ca: awsCaBundle.ca,
};
+64
View File
@@ -0,0 +1,64 @@
'use strict';
module.exports = {
0x00: 'DECIMAL', // aka DECIMAL
0x01: 'TINY', // aka TINYINT, 1 byte
0x02: 'SHORT', // aka SMALLINT, 2 bytes
0x03: 'LONG', // aka INT, 4 bytes
0x04: 'FLOAT', // aka FLOAT, 4-8 bytes
0x05: 'DOUBLE', // aka DOUBLE, 8 bytes
0x06: 'NULL', // NULL (used for prepared statements, I think)
0x07: 'TIMESTAMP', // aka TIMESTAMP
0x08: 'LONGLONG', // aka BIGINT, 8 bytes
0x09: 'INT24', // aka MEDIUMINT, 3 bytes
0x0a: 'DATE', // aka DATE
0x0b: 'TIME', // aka TIME
0x0c: 'DATETIME', // aka DATETIME
0x0d: 'YEAR', // aka YEAR, 1 byte (don't ask)
0x0e: 'NEWDATE', // aka ?
0x0f: 'VARCHAR', // aka VARCHAR (?)
0x10: 'BIT', // aka BIT, 1-8 byte
0xf5: 'JSON',
0xf6: 'NEWDECIMAL', // aka DECIMAL
0xf7: 'ENUM', // aka ENUM
0xf8: 'SET', // aka SET
0xf9: 'TINY_BLOB', // aka TINYBLOB, TINYTEXT
0xfa: 'MEDIUM_BLOB', // aka MEDIUMBLOB, MEDIUMTEXT
0xfb: 'LONG_BLOB', // aka LONGBLOG, LONGTEXT
0xfc: 'BLOB', // aka BLOB, TEXT
0xfd: 'VAR_STRING', // aka VARCHAR, VARBINARY
0xfe: 'STRING', // aka CHAR, BINARY
0xff: 'GEOMETRY', // aka GEOMETRY
};
// Manually extracted from mysql-5.5.23/include/mysql_com.h
// some more info here: http://dev.mysql.com/doc/refman/5.5/en/c-api-prepared-statement-type-codes.html
module.exports.DECIMAL = 0x00; // aka DECIMAL (http://dev.mysql.com/doc/refman/5.0/en/precision-math-decimal-changes.html)
module.exports.TINY = 0x01; // aka TINYINT, 1 byte
module.exports.SHORT = 0x02; // aka SMALLINT, 2 bytes
module.exports.LONG = 0x03; // aka INT, 4 bytes
module.exports.FLOAT = 0x04; // aka FLOAT, 4-8 bytes
module.exports.DOUBLE = 0x05; // aka DOUBLE, 8 bytes
module.exports.NULL = 0x06; // NULL (used for prepared statements, I think)
module.exports.TIMESTAMP = 0x07; // aka TIMESTAMP
module.exports.LONGLONG = 0x08; // aka BIGINT, 8 bytes
module.exports.INT24 = 0x09; // aka MEDIUMINT, 3 bytes
module.exports.DATE = 0x0a; // aka DATE
module.exports.TIME = 0x0b; // aka TIME
module.exports.DATETIME = 0x0c; // aka DATETIME
module.exports.YEAR = 0x0d; // aka YEAR, 1 byte (don't ask)
module.exports.NEWDATE = 0x0e; // aka ?
module.exports.VARCHAR = 0x0f; // aka VARCHAR (?)
module.exports.BIT = 0x10; // aka BIT, 1-8 byte
module.exports.VECTOR = 0xf2;
module.exports.JSON = 0xf5;
module.exports.NEWDECIMAL = 0xf6; // aka DECIMAL
module.exports.ENUM = 0xf7; // aka ENUM
module.exports.SET = 0xf8; // aka SET
module.exports.TINY_BLOB = 0xf9; // aka TINYBLOB, TINYTEXT
module.exports.MEDIUM_BLOB = 0xfa; // aka MEDIUMBLOB, MEDIUMTEXT
module.exports.LONG_BLOB = 0xfb; // aka LONGBLOG, LONGTEXT
module.exports.BLOB = 0xfc; // aka BLOB, TEXT
module.exports.VAR_STRING = 0xfd; // aka VARCHAR, VARBINARY
module.exports.STRING = 0xfe; // aka CHAR, BINARY
module.exports.GEOMETRY = 0xff; // aka GEOMETRY
+10
View File
@@ -0,0 +1,10 @@
'use strict';
const Connection = require('./connection.js');
const ConnectionConfig = require('./connection_config.js');
function createConnection(opts) {
return new Connection({ config: new ConnectionConfig(opts) });
}
module.exports = createConnection;
+10
View File
@@ -0,0 +1,10 @@
'use strict';
const Pool = require('./pool.js');
const PoolConfig = require('./pool_config.js');
function createPool(config) {
return new Pool({ config: new PoolConfig(config) });
}
module.exports = createPool;
+9
View File
@@ -0,0 +1,9 @@
'use strict';
const PoolCluster = require('./pool_cluster.js');
function createPoolCluster(config) {
return new PoolCluster(config);
}
module.exports = createPoolCluster;
+83
View File
@@ -0,0 +1,83 @@
'use strict';
/*
this seems to be not only shorter, but faster than
string.replace(/\\/g, '\\\\').
replace(/\u0008/g, '\\b').
replace(/\t/g, '\\t').
replace(/\n/g, '\\n').
replace(/\f/g, '\\f').
replace(/\r/g, '\\r').
replace(/'/g, '\\\'').
replace(/"/g, '\\"');
or string.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&")
see http://jsperf.com/string-escape-regexp-vs-json-stringify
*/
function srcEscape(str) {
return JSON.stringify({
[str]: 1,
}).slice(1, -3);
}
exports.srcEscape = srcEscape;
let highlightFn;
let cardinalRecommended = false;
try {
// the purpose of this is to prevent projects using Webpack from displaying a warning during runtime if cardinal is not a dependency
const REQUIRE_TERMINATOR = '';
highlightFn = require(`cardinal${REQUIRE_TERMINATOR}`).highlight;
} catch {
highlightFn = (text) => {
if (!cardinalRecommended) {
console.log('For nicer debug output consider install cardinal@^2.0.0');
cardinalRecommended = true;
}
return text;
};
}
/**
* Prints debug message with code frame, will try to use `cardinal` if available.
*/
function printDebugWithCode(msg, code) {
console.log(`\n\n${msg}:\n`);
console.log(`${highlightFn(code)}\n`);
}
exports.printDebugWithCode = printDebugWithCode;
/**
* checks whether the `type` is in the `list`
*/
function typeMatch(type, list, Types) {
if (Array.isArray(list)) {
return list.some((t) => type === Types[t]);
}
return !!list;
}
exports.typeMatch = typeMatch;
const privateObjectProps = new Set([
'__defineGetter__',
'__defineSetter__',
'__lookupGetter__',
'__lookupSetter__',
'__proto__',
]);
exports.privateObjectProps = privateObjectProps;
const fieldEscape = (field, isEval = true) => {
if (privateObjectProps.has(field)) {
throw new Error(
`The field name (${field}) can't be the same as an object's private property.`
);
}
return isEval ? srcEscape(field) : field;
};
exports.fieldEscape = fieldEscape;
+195
View File
@@ -0,0 +1,195 @@
'use strict';
const Packet = require('./packets/packet.js');
const MAX_PACKET_LENGTH = 16777215;
function readPacketLength(b, off) {
const b0 = b[off];
const b1 = b[off + 1];
const b2 = b[off + 2];
if (b1 + b2 === 0) {
return b0;
}
return b0 + (b1 << 8) + (b2 << 16);
}
class PacketParser {
constructor(onPacket, packetHeaderLength) {
// 4 for normal packets, 7 for comprssed protocol packets
if (typeof packetHeaderLength === 'undefined') {
packetHeaderLength = 4;
}
// array of last payload chunks
// only used when current payload is not complete
this.buffer = [];
// total length of chunks on buffer
this.bufferLength = 0;
this.packetHeaderLength = packetHeaderLength;
// incomplete header state: number of header bytes received
this.headerLen = 0;
// expected payload length
this.length = 0;
this.largePacketParts = [];
this.firstPacketSequenceId = 0;
this.onPacket = onPacket;
this.execute = PacketParser.prototype.executeStart;
this._flushLargePacket =
packetHeaderLength === 7
? this._flushLargePacket7
: this._flushLargePacket4;
}
_flushLargePacket4() {
const numPackets = this.largePacketParts.length;
this.largePacketParts.unshift(Buffer.from([0, 0, 0, 0])); // insert header
const body = Buffer.concat(this.largePacketParts);
const packet = new Packet(this.firstPacketSequenceId, body, 0, body.length);
this.largePacketParts.length = 0;
packet.numPackets = numPackets;
this.onPacket(packet);
}
_flushLargePacket7() {
const numPackets = this.largePacketParts.length;
this.largePacketParts.unshift(Buffer.from([0, 0, 0, 0, 0, 0, 0])); // insert header
const body = Buffer.concat(this.largePacketParts);
this.largePacketParts.length = 0;
const packet = new Packet(this.firstPacketSequenceId, body, 0, body.length);
packet.numPackets = numPackets;
this.onPacket(packet);
}
executeStart(chunk) {
let start = 0;
const end = chunk.length;
while (end - start >= 3) {
this.length = readPacketLength(chunk, start);
if (end - start >= this.length + this.packetHeaderLength) {
// at least one full packet
const sequenceId = chunk[start + 3];
if (
this.length < MAX_PACKET_LENGTH &&
this.largePacketParts.length === 0
) {
this.onPacket(
new Packet(
sequenceId,
chunk,
start,
start + this.packetHeaderLength + this.length
)
);
} else {
// first large packet - remember it's id
if (this.largePacketParts.length === 0) {
this.firstPacketSequenceId = sequenceId;
}
this.largePacketParts.push(
chunk.slice(
start + this.packetHeaderLength,
start + this.packetHeaderLength + this.length
)
);
if (this.length < MAX_PACKET_LENGTH) {
this._flushLargePacket();
}
}
start += this.packetHeaderLength + this.length;
} else {
// payload is incomplete
this.buffer = [chunk.slice(start + 3, end)];
this.bufferLength = end - start - 3;
this.execute = PacketParser.prototype.executePayload;
return;
}
}
if (end - start > 0) {
// there is start of length header, but it's not full 3 bytes
this.headerLen = end - start; // 1 or 2 bytes
this.length = chunk[start];
if (this.headerLen === 2) {
this.length = chunk[start] + (chunk[start + 1] << 8);
this.execute = PacketParser.prototype.executeHeader3;
} else {
this.execute = PacketParser.prototype.executeHeader2;
}
}
}
executePayload(chunk) {
let start = 0;
const end = chunk.length;
const remainingPayload =
this.length - this.bufferLength + this.packetHeaderLength - 3;
if (end - start >= remainingPayload) {
// last chunk for payload
const payload = Buffer.allocUnsafe(this.length + this.packetHeaderLength);
let offset = 3;
for (let i = 0; i < this.buffer.length; ++i) {
this.buffer[i].copy(payload, offset);
offset += this.buffer[i].length;
}
chunk.copy(payload, offset, start, start + remainingPayload);
const sequenceId = payload[3];
if (
this.length < MAX_PACKET_LENGTH &&
this.largePacketParts.length === 0
) {
this.onPacket(
new Packet(
sequenceId,
payload,
0,
this.length + this.packetHeaderLength
)
);
} else {
// first large packet - remember it's id
if (this.largePacketParts.length === 0) {
this.firstPacketSequenceId = sequenceId;
}
this.largePacketParts.push(
payload.slice(
this.packetHeaderLength,
this.packetHeaderLength + this.length
)
);
if (this.length < MAX_PACKET_LENGTH) {
this._flushLargePacket();
}
}
this.buffer = [];
this.bufferLength = 0;
this.execute = PacketParser.prototype.executeStart;
start += remainingPayload;
if (end - start > 0) {
return this.execute(chunk.slice(start, end));
}
} else {
this.buffer.push(chunk);
this.bufferLength += chunk.length;
}
return null;
}
executeHeader2(chunk) {
this.length += chunk[0] << 8;
if (chunk.length > 1) {
this.length += chunk[1] << 16;
this.execute = PacketParser.prototype.executePayload;
return this.executePayload(chunk.slice(2));
}
this.execute = PacketParser.prototype.executeHeader3;
return null;
}
executeHeader3(chunk) {
this.length += chunk[0] << 16;
this.execute = PacketParser.prototype.executePayload;
return this.executePayload(chunk.slice(1));
}
}
module.exports = PacketParser;
+35
View File
@@ -0,0 +1,35 @@
// Copyright (c) 2021, Oracle and/or its affiliates.
'use strict';
const Packet = require('../packets/packet');
class AuthNextFactor {
constructor(opts) {
this.pluginName = opts.pluginName;
this.pluginData = opts.pluginData;
}
toPacket(encoding) {
const length = 6 + this.pluginName.length + this.pluginData.length;
const buffer = Buffer.allocUnsafe(length);
const packet = new Packet(0, buffer, 0, length);
packet.offset = 4;
packet.writeInt8(0x02);
packet.writeNullTerminatedString(this.pluginName, encoding);
packet.writeBuffer(this.pluginData);
return packet;
}
static fromPacket(packet, encoding) {
packet.readInt8(); // marker
const name = packet.readNullTerminatedString(encoding);
const data = packet.readBuffer();
return new AuthNextFactor({
pluginName: name,
pluginData: data,
});
}
}
module.exports = AuthNextFactor;
+38
View File
@@ -0,0 +1,38 @@
'use strict';
// http://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::AuthSwitchRequest
const Packet = require('../packets/packet');
class AuthSwitchRequest {
constructor(opts) {
this.pluginName = opts.pluginName;
this.pluginData = opts.pluginData;
}
toPacket() {
const length = 6 + this.pluginName.length + this.pluginData.length;
const buffer = Buffer.allocUnsafe(length);
const packet = new Packet(0, buffer, 0, length);
packet.offset = 4;
packet.writeInt8(0xfe);
// TODO: use server encoding
packet.writeNullTerminatedString(this.pluginName, 'cesu8');
packet.writeBuffer(this.pluginData);
return packet;
}
static fromPacket(packet) {
packet.readInt8(); // marker
// assert marker == 0xfe?
// TODO: use server encoding
const name = packet.readNullTerminatedString('cesu8');
const data = packet.readBuffer();
return new AuthSwitchRequest({
pluginName: name,
pluginData: data,
});
}
}
module.exports = AuthSwitchRequest;
@@ -0,0 +1,33 @@
'use strict';
// http://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::AuthSwitchRequest
const Packet = require('../packets/packet');
class AuthSwitchRequestMoreData {
constructor(data) {
this.data = data;
}
toPacket() {
const length = 5 + this.data.length;
const buffer = Buffer.allocUnsafe(length);
const packet = new Packet(0, buffer, 0, length);
packet.offset = 4;
packet.writeInt8(0x01);
packet.writeBuffer(this.data);
return packet;
}
static fromPacket(packet) {
packet.readInt8(); // marker
const data = packet.readBuffer();
return new AuthSwitchRequestMoreData(data);
}
static verifyMarker(packet) {
return packet.peekByte() === 0x01;
}
}
module.exports = AuthSwitchRequestMoreData;
+30
View File
@@ -0,0 +1,30 @@
'use strict';
// http://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::AuthSwitchRequest
const Packet = require('../packets/packet');
class AuthSwitchResponse {
constructor(data) {
if (!Buffer.isBuffer(data)) {
data = Buffer.from(data);
}
this.data = data;
}
toPacket() {
const length = 4 + this.data.length;
const buffer = Buffer.allocUnsafe(length);
const packet = new Packet(0, buffer, 0, length);
packet.offset = 4;
packet.writeBuffer(this.data);
return packet;
}
static fromPacket(packet) {
const data = packet.readBuffer();
return new AuthSwitchResponse(data);
}
}
module.exports = AuthSwitchResponse;
+95
View File
@@ -0,0 +1,95 @@
'use strict';
const Types = require('../constants/types');
const Packet = require('../packets/packet');
const binaryReader = new Array(256);
class BinaryRow {
constructor(columns) {
this.columns = columns || [];
}
static toPacket(columns, encoding) {
// throw new Error('Not implemented');
const sequenceId = 0; // TODO remove, this is calculated now in connecton
let length = 0;
columns.forEach((val) => {
if (val === null || typeof val === 'undefined') {
++length;
return;
}
length += Packet.lengthCodedStringLength(val.toString(10), encoding);
});
length = length + 2;
const buffer = Buffer.allocUnsafe(length + 4);
const packet = new Packet(sequenceId, buffer, 0, length + 4);
packet.offset = 4;
packet.writeInt8(0);
let bitmap = 0;
let bitValue = 1;
columns.forEach((parameter) => {
if (parameter.type === Types.NULL) {
bitmap += bitValue;
}
bitValue *= 2;
if (bitValue === 256) {
packet.writeInt8(bitmap);
bitmap = 0;
bitValue = 1;
}
});
if (bitValue !== 1) {
packet.writeInt8(bitmap);
}
columns.forEach((val) => {
if (val === null) {
packet.writeNull();
return;
}
if (typeof val === 'undefined') {
packet.writeInt8(0);
return;
}
packet.writeLengthCodedString(val.toString(10), encoding);
});
return packet;
}
// TODO: complete list of types...
static fromPacket(fields, packet) {
const columns = new Array(fields.length);
packet.readInt8(); // TODO check it's 0
const nullBitmapLength = Math.floor((fields.length + 7 + 2) / 8);
// TODO: read and interpret null bitmap
packet.skip(nullBitmapLength);
for (let i = 0; i < columns.length; ++i) {
columns[i] = binaryReader[fields[i].columnType].apply(packet);
}
return new BinaryRow(columns);
}
}
// TODO: replace with constants.MYSQL_TYPE_*
binaryReader[Types.DECIMAL] = Packet.prototype.readLengthCodedString;
binaryReader[1] = Packet.prototype.readInt8; // tiny
binaryReader[2] = Packet.prototype.readInt16; // short
binaryReader[3] = Packet.prototype.readInt32; // long
binaryReader[4] = Packet.prototype.readFloat; // float
binaryReader[5] = Packet.prototype.readDouble; // double
binaryReader[6] = Packet.prototype.assertInvalid; // null, should be skipped vie null bitmap
binaryReader[7] = Packet.prototype.readTimestamp; // timestamp, http://dev.mysql.com/doc/internals/en/prepared-statements.html#packet-ProtocolBinary::MYSQL_TYPE_TIMESTAMP
binaryReader[8] = Packet.prototype.readInt64; // long long
binaryReader[9] = Packet.prototype.readInt32; // int24
binaryReader[10] = Packet.prototype.readTimestamp; // date
binaryReader[11] = Packet.prototype.readTime; // time, http://dev.mysql.com/doc/internals/en/prepared-statements.html#packet-ProtocolBinary::MYSQL_TYPE_TIME
binaryReader[12] = Packet.prototype.readDateTime; // datetime, http://dev.mysql.com/doc/internals/en/prepared-statements.html#packet-ProtocolBinary::MYSQL_TYPE_DATETIME
binaryReader[13] = Packet.prototype.readInt16; // year
binaryReader[Types.VAR_STRING] = Packet.prototype.readLengthCodedString; // var string
module.exports = BinaryRow;
+33
View File
@@ -0,0 +1,33 @@
'use strict';
// http://dev.mysql.com/doc/internals/en/com-binlog-dump.html#packet-COM_BINLOG_DUMP
const Packet = require('../packets/packet');
const CommandCodes = require('../constants/commands');
// TODO: add flag to constants
// 0x01 - BINLOG_DUMP_NON_BLOCK
// send EOF instead of blocking
class BinlogDump {
constructor(opts) {
this.binlogPos = opts.binlogPos || 0;
this.serverId = opts.serverId || 0;
this.flags = opts.flags || 0;
this.filename = opts.filename || '';
}
toPacket() {
const length = 15 + Buffer.byteLength(this.filename, 'utf8'); // TODO: should be ascii?
const buffer = Buffer.allocUnsafe(length);
const packet = new Packet(0, buffer, 0, length);
packet.offset = 4;
packet.writeInt8(CommandCodes.BINLOG_DUMP);
packet.writeInt32(this.binlogPos);
packet.writeInt16(this.flags);
packet.writeInt32(this.serverId);
packet.writeString(this.filename);
return packet;
}
}
module.exports = BinlogDump;
+115
View File
@@ -0,0 +1,115 @@
'use strict';
// http://dev.mysql.com/doc/internals/en/query-event.html
const keys = {
FLAGS2: 0,
SQL_MODE: 1,
CATALOG: 2,
AUTO_INCREMENT: 3,
CHARSET: 4,
TIME_ZONE: 5,
CATALOG_NZ: 6,
LC_TIME_NAMES: 7,
CHARSET_DATABASE: 8,
TABLE_MAP_FOR_UPDATE: 9,
MASTER_DATA_WRITTEN: 10,
INVOKERS: 11,
UPDATED_DB_NAMES: 12,
MICROSECONDS: 3,
};
module.exports = function parseStatusVars(buffer) {
const result = {};
let offset = 0;
let key, length, prevOffset;
while (offset < buffer.length) {
key = buffer[offset++];
switch (key) {
case keys.FLAGS2:
result.flags = buffer.readUInt32LE(offset);
offset += 4;
break;
case keys.SQL_MODE:
// value is 8 bytes, but all dcumented flags are in first 4 bytes
result.sqlMode = buffer.readUInt32LE(offset);
offset += 8;
break;
case keys.CATALOG:
length = buffer[offset++];
result.catalog = buffer.toString('utf8', offset, offset + length);
offset += length + 1; // null byte after string
break;
case keys.CHARSET:
result.clientCharset = buffer.readUInt16LE(offset);
result.connectionCollation = buffer.readUInt16LE(offset + 2);
result.serverCharset = buffer.readUInt16LE(offset + 4);
offset += 6;
break;
case keys.TIME_ZONE:
length = buffer[offset++];
result.timeZone = buffer.toString('utf8', offset, offset + length);
offset += length; // no null byte
break;
case keys.CATALOG_NZ:
length = buffer[offset++];
result.catalogNz = buffer.toString('utf8', offset, offset + length);
offset += length; // no null byte
break;
case keys.LC_TIME_NAMES:
result.lcTimeNames = buffer.readUInt16LE(offset);
offset += 2;
break;
case keys.CHARSET_DATABASE:
result.schemaCharset = buffer.readUInt16LE(offset);
offset += 2;
break;
case keys.TABLE_MAP_FOR_UPDATE:
result.mapForUpdate1 = buffer.readUInt32LE(offset);
result.mapForUpdate2 = buffer.readUInt32LE(offset + 4);
offset += 8;
break;
case keys.MASTER_DATA_WRITTEN:
result.masterDataWritten = buffer.readUInt32LE(offset);
offset += 4;
break;
case keys.INVOKERS:
length = buffer[offset++];
result.invokerUsername = buffer.toString(
'utf8',
offset,
offset + length
);
offset += length;
length = buffer[offset++];
result.invokerHostname = buffer.toString(
'utf8',
offset,
offset + length
);
offset += length;
break;
case keys.UPDATED_DB_NAMES:
length = buffer[offset++];
// length - number of null-terminated strings
result.updatedDBs = []; // we'll store them as array here
for (; length; --length) {
prevOffset = offset;
// fast forward to null terminating byte
while (buffer[offset++] && offset < buffer.length) {
// empty body, everything inside while condition
}
result.updatedDBs.push(
buffer.toString('utf8', prevOffset, offset - 1)
);
}
break;
case keys.MICROSECONDS:
result.microseconds =
// REVIEW: INVALID UNKNOWN VARIABLE!
buffer.readInt16LE(offset) + (buffer[offset + 2] << 16);
offset += 3;
}
}
return result;
};
+97
View File
@@ -0,0 +1,97 @@
'use strict';
const CommandCode = require('../constants/commands.js');
const ClientConstants = require('../constants/client.js');
const Packet = require('../packets/packet.js');
const auth41 = require('../auth_41.js');
const CharsetToEncoding = require('../constants/charset_encodings.js');
// https://dev.mysql.com/doc/internals/en/com-change-user.html#packet-COM_CHANGE_USER
class ChangeUser {
constructor(opts) {
this.flags = opts.flags;
this.user = opts.user || '';
this.database = opts.database || '';
this.password = opts.password || '';
this.passwordSha1 = opts.passwordSha1;
this.authPluginData1 = opts.authPluginData1;
this.authPluginData2 = opts.authPluginData2;
this.connectAttributes = opts.connectAttrinutes || {};
let authToken;
if (this.passwordSha1) {
authToken = auth41.calculateTokenFromPasswordSha(
this.passwordSha1,
this.authPluginData1,
this.authPluginData2
);
} else {
authToken = auth41.calculateToken(
this.password,
this.authPluginData1,
this.authPluginData2
);
}
this.authToken = authToken;
this.charsetNumber = opts.charsetNumber;
}
// TODO
// ChangeUser.fromPacket = function(packet)
// };
serializeToBuffer(buffer) {
const isSet = (flag) => this.flags & ClientConstants[flag];
const packet = new Packet(0, buffer, 0, buffer.length);
packet.offset = 4;
const encoding = CharsetToEncoding[this.charsetNumber];
packet.writeInt8(CommandCode.CHANGE_USER);
packet.writeNullTerminatedString(this.user, encoding);
if (isSet('SECURE_CONNECTION')) {
packet.writeInt8(this.authToken.length);
packet.writeBuffer(this.authToken);
} else {
packet.writeBuffer(this.authToken);
packet.writeInt8(0);
}
packet.writeNullTerminatedString(this.database, encoding);
packet.writeInt16(this.charsetNumber);
if (isSet('PLUGIN_AUTH')) {
// TODO: read this from parameters
packet.writeNullTerminatedString('mysql_native_password', 'latin1');
}
if (isSet('CONNECT_ATTRS')) {
const connectAttributes = this.connectAttributes;
const attrNames = Object.keys(connectAttributes);
let keysLength = 0;
for (let k = 0; k < attrNames.length; ++k) {
keysLength += Packet.lengthCodedStringLength(attrNames[k], encoding);
keysLength += Packet.lengthCodedStringLength(
connectAttributes[attrNames[k]],
encoding
);
}
packet.writeLengthCodedNumber(keysLength);
for (let k = 0; k < attrNames.length; ++k) {
packet.writeLengthCodedString(attrNames[k], encoding);
packet.writeLengthCodedString(
connectAttributes[attrNames[k]],
encoding
);
}
}
return packet;
}
toPacket() {
if (typeof this.user !== 'string') {
throw new Error('"user" connection config property must be a string');
}
if (typeof this.database !== 'string') {
throw new Error('"database" connection config property must be a string');
}
// dry run: calculate resulting packet length
const p = this.serializeToBuffer(Packet.MockBuffer());
return this.serializeToBuffer(Buffer.allocUnsafe(p.offset));
}
}
module.exports = ChangeUser;
+21
View File
@@ -0,0 +1,21 @@
'use strict';
const Packet = require('../packets/packet');
const CommandCodes = require('../constants/commands');
class CloseStatement {
constructor(id) {
this.id = id;
}
// note: no response sent back
toPacket() {
const packet = new Packet(0, Buffer.allocUnsafe(9), 0, 9);
packet.offset = 4;
packet.writeInt8(CommandCodes.STMT_CLOSE);
packet.writeInt32(this.id);
return packet;
}
}
module.exports = CloseStatement;
+291
View File
@@ -0,0 +1,291 @@
'use strict';
const Packet = require('../packets/packet');
const StringParser = require('../parsers/string');
const CharsetToEncoding = require('../constants/charset_encodings.js');
const fields = ['catalog', 'schema', 'table', 'orgTable', 'name', 'orgName'];
// creating JS string is relatively expensive (compared to
// reading few bytes from buffer) because all string properties
// except for name are unlikely to be used we postpone
// string conversion until property access
//
// TODO: watch for integration benchmarks (one with real network buffer)
// there could be bad side effect as keeping reference to a buffer makes it
// sit in the memory longer (usually until final .query() callback)
// Latest v8 perform much better in regard to bufferer -> string conversion,
// at some point of time this optimisation might become unnecessary
// see https://github.com/sidorares/node-mysql2/pull/137
//
class ColumnDefinition {
constructor(packet, clientEncoding) {
this._buf = packet.buffer;
this._clientEncoding = clientEncoding;
this._catalogLength = packet.readLengthCodedNumber();
this._catalogStart = packet.offset;
packet.offset += this._catalogLength;
this._schemaLength = packet.readLengthCodedNumber();
this._schemaStart = packet.offset;
packet.offset += this._schemaLength;
this._tableLength = packet.readLengthCodedNumber();
this._tableStart = packet.offset;
packet.offset += this._tableLength;
this._orgTableLength = packet.readLengthCodedNumber();
this._orgTableStart = packet.offset;
packet.offset += this._orgTableLength;
// name is always used, don't make it lazy
const _nameLength = packet.readLengthCodedNumber();
const _nameStart = packet.offset;
packet.offset += _nameLength;
this._orgNameLength = packet.readLengthCodedNumber();
this._orgNameStart = packet.offset;
packet.offset += this._orgNameLength;
packet.skip(1); // length of the following fields (always 0x0c)
this.characterSet = packet.readInt16();
this.encoding = CharsetToEncoding[this.characterSet];
this.name = StringParser.decode(
this._buf,
this.encoding === 'binary' ? this._clientEncoding : this.encoding,
_nameStart,
_nameStart + _nameLength
);
this.columnLength = packet.readInt32();
this.columnType = packet.readInt8();
this.type = this.columnType;
this.flags = packet.readInt16();
this.decimals = packet.readInt8();
}
inspect() {
return {
catalog: this.catalog,
schema: this.schema,
name: this.name,
orgName: this.orgName,
table: this.table,
orgTable: this.orgTable,
characterSet: this.characterSet,
encoding: this.encoding,
columnLength: this.columnLength,
type: this.columnType,
flags: this.flags,
decimals: this.decimals,
};
}
[Symbol.for('nodejs.util.inspect.custom')](depth, inspectOptions, inspect) {
const Types = require('../constants/types.js');
const typeNames = [];
for (const t in Types) {
typeNames[Types[t]] = t;
}
const fiedFlags = require('../constants/field_flags.js');
const flagNames = [];
// TODO: respect inspectOptions.showHidden
//const inspectFlags = inspectOptions.showHidden ? this.flags : this.flags & ~fiedFlags.PRI_KEY;
const inspectFlags = this.flags;
for (const f in fiedFlags) {
if (inspectFlags & fiedFlags[f]) {
if (f === 'PRI_KEY') {
flagNames.push('PRIMARY KEY');
} else if (f === 'NOT_NULL') {
flagNames.push('NOT NULL');
} else if (f === 'BINARY') {
// ignore flag for now
} else if (f === 'MULTIPLE_KEY') {
// not sure if that should be part of inspection.
// in the schema usually this is part of index definition
// example: UNIQUE KEY `my_uniq_id` (`id_box_elements`,`id_router`)
// note that only first column has MULTIPLE_KEY flag set in this case
// so there is no good way of knowing that this is part of index just
// by looking at indifidual field flags
} else if (f === 'NO_DEFAULT_VALUE') {
// almost the same as NOT_NULL?
} else if (f === 'BLOB') {
// included in the type
} else if (f === 'UNSIGNED') {
// this should be first after type
} else if (f === 'TIMESTAMP') {
// timestamp flag is redundant for inspection - already included in type
} else if (f === 'ON_UPDATE_NOW') {
flagNames.push('ON UPDATE CURRENT_TIMESTAMP');
} else {
flagNames.push(f);
}
}
}
if (depth > 1) {
return inspect({
...this.inspect(),
typeName: typeNames[this.columnType],
flags: flagNames,
});
}
const isUnsigned = this.flags & fiedFlags.UNSIGNED;
let typeName = typeNames[this.columnType];
if (typeName === 'BLOB') {
// TODO: check for non-utf8mb4 encoding
if (this.columnLength === 4294967295) {
typeName = 'LONGTEXT';
} else if (this.columnLength === 67108860) {
typeName = 'MEDIUMTEXT';
} else if (this.columnLength === 262140) {
typeName = 'TEXT';
} else if (this.columnLength === 1020) {
// 255*4
typeName = 'TINYTEXT';
} else {
typeName = `BLOB(${this.columnLength})`;
}
} else if (typeName === 'VAR_STRING') {
// TODO: check for non-utf8mb4 encoding
typeName = `VARCHAR(${Math.ceil(this.columnLength / 4)})`;
} else if (typeName === 'TINY') {
if (
(this.columnLength === 3 && isUnsigned) ||
(this.columnLength === 4 && !isUnsigned)
) {
typeName = 'TINYINT';
} else {
typeName = `TINYINT(${this.columnLength})`;
}
} else if (typeName === 'LONGLONG') {
if (this.columnLength === 20) {
typeName = 'BIGINT';
} else {
typeName = `BIGINT(${this.columnLength})`;
}
} else if (typeName === 'SHORT') {
if (isUnsigned && this.columnLength === 5) {
typeName = 'SMALLINT';
} else if (!isUnsigned && this.columnLength === 6) {
typeName = 'SMALLINT';
} else {
typeName = `SMALLINT(${this.columnLength})`;
}
} else if (typeName === 'LONG') {
if (isUnsigned && this.columnLength === 10) {
typeName = 'INT';
} else if (!isUnsigned && this.columnLength === 11) {
typeName = 'INT';
} else {
typeName = `INT(${this.columnLength})`;
}
} else if (typeName === 'INT24') {
if (isUnsigned && this.columnLength === 8) {
typeName = 'MEDIUMINT';
} else if (!isUnsigned && this.columnLength === 9) {
typeName = 'MEDIUMINT';
} else {
typeName = `MEDIUMINT(${this.columnLength})`;
}
} else if (typeName === 'DOUBLE') {
// DOUBLE without modifiers is reported as DOUBLE(22, 31)
if (this.columnLength === 22 && this.decimals === 31) {
typeName = 'DOUBLE';
} else {
typeName = `DOUBLE(${this.columnLength},${this.decimals})`;
}
} else if (typeName === 'FLOAT') {
// FLOAT without modifiers is reported as FLOAT(12, 31)
if (this.columnLength === 12 && this.decimals === 31) {
typeName = 'FLOAT';
} else {
typeName = `FLOAT(${this.columnLength},${this.decimals})`;
}
} else if (typeName === 'NEWDECIMAL') {
if (this.columnLength === 11 && this.decimals === 0) {
typeName = 'DECIMAL';
} else if (this.decimals === 0) {
// not sure why, but DECIMAL(13) is reported as DECIMAL(14, 0)
// and DECIMAL(13, 9) is reported as NEWDECIMAL(15, 9)
if (isUnsigned) {
typeName = `DECIMAL(${this.columnLength})`;
} else {
typeName = `DECIMAL(${this.columnLength - 1})`;
}
} else {
typeName = `DECIMAL(${this.columnLength - 2},${this.decimals})`;
}
} else {
typeName = `${typeNames[this.columnType]}(${this.columnLength})`;
}
if (isUnsigned) {
typeName += ' UNSIGNED';
}
// TODO respect colors option
return `\`${this.name}\` ${[typeName, ...flagNames].join(' ')}`;
}
static toPacket(column, sequenceId) {
let length = 17; // = 4 padding + 1 + 12 for the rest
fields.forEach((field) => {
length += Packet.lengthCodedStringLength(
column[field],
CharsetToEncoding[column.characterSet]
);
});
const buffer = Buffer.allocUnsafe(length);
const packet = new Packet(sequenceId, buffer, 0, length);
function writeField(name) {
packet.writeLengthCodedString(
column[name],
CharsetToEncoding[column.characterSet]
);
}
packet.offset = 4;
fields.forEach(writeField);
packet.writeInt8(0x0c);
packet.writeInt16(column.characterSet);
packet.writeInt32(column.columnLength);
packet.writeInt8(column.columnType);
packet.writeInt16(column.flags);
packet.writeInt8(column.decimals);
packet.writeInt16(0); // filler
return packet;
}
// node-mysql compatibility: alias "db" to "schema"
get db() {
return this.schema;
}
}
const addString = function (name) {
Object.defineProperty(ColumnDefinition.prototype, name, {
get: function () {
const start = this[`_${name}Start`];
const end = start + this[`_${name}Length`];
const val = StringParser.decode(
this._buf,
this.encoding === 'binary' ? this._clientEncoding : this.encoding,
start,
end
);
Object.defineProperty(this, name, {
value: val,
writable: false,
configurable: false,
enumerable: false,
});
return val;
},
});
};
addString('catalog');
addString('schema');
addString('table');
addString('orgTable');
addString('orgName');
module.exports = ColumnDefinition;
+69
View File
@@ -0,0 +1,69 @@
'use strict';
const Types = require('../constants/types');
const Packet = require('../packets/packet');
function isJSON(value) {
return (
Array.isArray(value) ||
value.constructor === Object ||
(typeof value.toJSON === 'function' && !Buffer.isBuffer(value))
);
}
function toParameter(value, encoding, timezone) {
let type = Types.VAR_STRING;
let length;
let writer = function (value) {
// eslint-disable-next-line no-invalid-this
return Packet.prototype.writeLengthCodedString.call(this, value, encoding);
};
if (value !== null) {
switch (typeof value) {
case 'undefined':
throw new TypeError('Bind parameters must not contain undefined');
case 'number':
type = Types.DOUBLE;
length = 8;
writer = Packet.prototype.writeDouble;
break;
case 'boolean':
value = value | 0;
type = Types.TINY;
length = 1;
writer = Packet.prototype.writeInt8;
break;
case 'object':
if (Object.prototype.toString.call(value) === '[object Date]') {
type = Types.DATETIME;
length = 12;
writer = function (value) {
// eslint-disable-next-line no-invalid-this
return Packet.prototype.writeDate.call(this, value, timezone);
};
} else if (isJSON(value)) {
value = JSON.stringify(value);
type = Types.JSON;
} else if (Buffer.isBuffer(value)) {
length = Packet.lengthCodedNumberLength(value.length) + value.length;
writer = Packet.prototype.writeLengthCodedBuffer;
}
break;
default:
value = value.toString();
}
} else {
value = '';
type = Types.NULL;
}
if (!length) {
length = Packet.lengthCodedStringLength(value, encoding);
}
return { value, type, length, writer };
}
module.exports = { toParameter, isJSON };
+177
View File
@@ -0,0 +1,177 @@
'use strict';
const CursorType = require('../constants/cursor');
const CommandCodes = require('../constants/commands');
const ClientConstants = require('../constants/client');
const Types = require('../constants/types');
const Packet = require('../packets/packet');
const CharsetToEncoding = require('../constants/charset_encodings.js');
const { toParameter } = require('./encode_parameter.js');
class Execute {
constructor(
id,
parameters,
charsetNumber,
timezone,
attributes,
clientFlags
) {
this.id = id;
this.parameters = parameters;
this.encoding = CharsetToEncoding[charsetNumber];
this.timezone = timezone;
this.attributes = attributes;
this.clientFlags = clientFlags || 0;
}
static fromPacket(packet, encoding) {
const stmtId = packet.readInt32();
const flags = packet.readInt8();
const iterationCount = packet.readInt32();
let i = packet.offset;
while (i < packet.end - 1) {
if (
(packet.buffer[i + 1] === Types.VAR_STRING ||
packet.buffer[i + 1] === Types.NULL ||
packet.buffer[i + 1] === Types.DOUBLE ||
packet.buffer[i + 1] === Types.TINY ||
packet.buffer[i + 1] === Types.DATETIME ||
packet.buffer[i + 1] === Types.JSON) &&
packet.buffer[i] === 1 &&
packet.buffer[i + 2] === 0
) {
break;
} else {
packet.readInt8();
}
i++;
}
const types = [];
for (let i = packet.offset + 1; i < packet.end - 1; i++) {
if (
(packet.buffer[i] === Types.VAR_STRING ||
packet.buffer[i] === Types.NULL ||
packet.buffer[i] === Types.DOUBLE ||
packet.buffer[i] === Types.TINY ||
packet.buffer[i] === Types.DATETIME ||
packet.buffer[i] === Types.JSON) &&
packet.buffer[i + 1] === 0
) {
types.push(packet.buffer[i]);
packet.skip(2);
}
}
packet.skip(1);
const values = [];
for (let i = 0; i < types.length; i++) {
if (types[i] === Types.VAR_STRING) {
values.push(packet.readLengthCodedString(encoding));
} else if (types[i] === Types.DOUBLE) {
values.push(packet.readDouble());
} else if (types[i] === Types.TINY) {
values.push(packet.readInt8());
} else if (types[i] === Types.DATETIME) {
values.push(packet.readDateTime());
} else if (types[i] === Types.JSON) {
values.push(JSON.parse(packet.readLengthCodedString(encoding)));
}
if (types[i] === Types.NULL) {
values.push(null);
}
}
return { stmtId, flags, iterationCount, values };
}
_serializeToBuffer(buffer) {
const useQueryAttributes =
this.clientFlags & ClientConstants.CLIENT_QUERY_ATTRIBUTES;
const attrNames =
useQueryAttributes && this.attributes ? Object.keys(this.attributes) : [];
const numParams = this.parameters ? this.parameters.length : 0;
const numAttrs = attrNames.length;
const totalParams = numParams + numAttrs;
const packet = new Packet(0, buffer, 0, buffer.length);
packet.offset = 4;
packet.writeInt8(CommandCodes.STMT_EXECUTE);
packet.writeInt32(this.id);
let cursorFlags = CursorType.NO_CURSOR;
if (useQueryAttributes) {
cursorFlags |= CursorType.PARAMETER_COUNT_AVAILABLE;
}
packet.writeInt8(cursorFlags);
packet.writeInt32(1); // iteration-count, always 1
if (useQueryAttributes) {
packet.writeLengthCodedNumber(totalParams);
}
if (totalParams > 0) {
const bindParams =
numParams > 0
? this.parameters.map((v) =>
toParameter(v, this.encoding, this.timezone)
)
: [];
const attrParams = attrNames.map((name) =>
toParameter(this.attributes[name], this.encoding, this.timezone)
);
const allParams = bindParams.concat(attrParams);
// null bitmap
let bitmap = 0;
let bitValue = 1;
allParams.forEach((parameter) => {
if (parameter.type === Types.NULL) {
bitmap += bitValue;
}
bitValue *= 2;
if (bitValue === 256) {
packet.writeInt8(bitmap);
bitmap = 0;
bitValue = 1;
}
});
if (bitValue !== 1) {
packet.writeInt8(bitmap);
}
packet.writeInt8(1); // new-params-bound-flag
// types (and names for attributes)
for (let i = 0; i < allParams.length; i++) {
packet.writeInt8(allParams[i].type);
packet.writeInt8(0); // unsigned flag
if (useQueryAttributes) {
const name = i < numParams ? '' : attrNames[i - numParams];
packet.writeLengthCodedString(name, this.encoding);
}
}
// values
allParams.forEach((parameter) => {
if (parameter.type !== Types.NULL) {
parameter.writer.call(packet, parameter.value);
}
});
}
return packet;
}
toPacket() {
const p = this._serializeToBuffer(Packet.MockBuffer());
return this._serializeToBuffer(Buffer.allocUnsafe(p.offset));
}
}
module.exports = Execute;
+112
View File
@@ -0,0 +1,112 @@
'use strict';
const Packet = require('../packets/packet');
const ClientConstants = require('../constants/client.js');
// https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::Handshake
class Handshake {
constructor(args) {
this.protocolVersion = args.protocolVersion;
this.serverVersion = args.serverVersion;
this.capabilityFlags = args.capabilityFlags;
this.connectionId = args.connectionId;
this.authPluginData1 = args.authPluginData1;
this.authPluginData2 = args.authPluginData2;
this.characterSet = args.characterSet;
this.statusFlags = args.statusFlags;
this.authPluginName = args.authPluginName;
}
setScrambleData(cb) {
require('crypto').randomBytes(20, (err, data) => {
if (err) {
cb(err);
return;
}
this.authPluginData1 = data.slice(0, 8);
this.authPluginData2 = data.slice(8, 20);
cb();
});
}
toPacket(sequenceId) {
const length = 68 + Buffer.byteLength(this.serverVersion, 'utf8');
const buffer = Buffer.alloc(length + 4, 0); // zero fill, 10 bytes filler later needs to contain zeros
const packet = new Packet(sequenceId, buffer, 0, length + 4);
packet.offset = 4;
packet.writeInt8(this.protocolVersion);
packet.writeString(this.serverVersion, 'cesu8');
packet.writeInt8(0);
packet.writeInt32(this.connectionId);
packet.writeBuffer(this.authPluginData1);
packet.writeInt8(0);
const capabilityFlagsBuffer = Buffer.allocUnsafe(4);
capabilityFlagsBuffer.writeUInt32LE(this.capabilityFlags, 0);
packet.writeBuffer(capabilityFlagsBuffer.slice(0, 2));
packet.writeInt8(this.characterSet);
packet.writeInt16(this.statusFlags);
packet.writeBuffer(capabilityFlagsBuffer.slice(2, 4));
packet.writeInt8(21); // authPluginDataLength
packet.skip(10);
packet.writeBuffer(this.authPluginData2);
packet.writeInt8(0);
packet.writeString('mysql_native_password', 'latin1');
packet.writeInt8(0);
return packet;
}
static fromPacket(packet) {
const args = {};
args.protocolVersion = packet.readInt8();
args.serverVersion = packet.readNullTerminatedString('cesu8');
args.connectionId = packet.readInt32();
args.authPluginData1 = packet.readBuffer(8);
packet.skip(1);
const capabilityFlagsBuffer = Buffer.allocUnsafe(4);
capabilityFlagsBuffer[0] = packet.readInt8();
capabilityFlagsBuffer[1] = packet.readInt8();
if (packet.haveMoreData()) {
args.characterSet = packet.readInt8();
args.statusFlags = packet.readInt16();
// upper 2 bytes
capabilityFlagsBuffer[2] = packet.readInt8();
capabilityFlagsBuffer[3] = packet.readInt8();
args.capabilityFlags = capabilityFlagsBuffer.readUInt32LE(0);
if (args.capabilityFlags & ClientConstants.PLUGIN_AUTH) {
args.authPluginDataLength = packet.readInt8();
} else {
args.authPluginDataLength = 0;
packet.skip(1);
}
packet.skip(10);
} else {
args.capabilityFlags = capabilityFlagsBuffer.readUInt16LE(0);
}
const isSecureConnection =
args.capabilityFlags & ClientConstants.SECURE_CONNECTION;
if (isSecureConnection) {
const authPluginDataLength = args.authPluginDataLength;
if (authPluginDataLength === 0) {
// for Secure Password Authentication
args.authPluginDataLength = 20;
args.authPluginData2 = packet.readBuffer(12);
packet.skip(1);
} else {
// length > 0
// for Custom Auth Plugin (PLUGIN_AUTH)
const len = Math.max(13, authPluginDataLength - 8);
args.authPluginData2 = packet.readBuffer(len);
}
}
if (args.capabilityFlags & ClientConstants.PLUGIN_AUTH) {
args.authPluginName = packet.readNullTerminatedString('ascii');
}
return new Handshake(args);
}
}
module.exports = Handshake;
+173
View File
@@ -0,0 +1,173 @@
'use strict';
const ClientConstants = require('../constants/client.js');
const CharsetToEncoding = require('../constants/charset_encodings.js');
const Packet = require('../packets/packet.js');
const auth41 = require('../auth_41.js');
class HandshakeResponse {
constructor(handshake) {
this.user = handshake.user || '';
this.database = handshake.database || '';
this.password = handshake.password || '';
this.passwordSha1 = handshake.passwordSha1;
this.authPluginData1 = handshake.authPluginData1;
this.authPluginData2 = handshake.authPluginData2;
this.compress = handshake.compress;
this.clientFlags = handshake.flags;
// Accept pre-calculated authToken and authPluginName from caller
// This allows the caller to optimize by using the server's preferred auth method
if (
handshake.authToken !== undefined &&
handshake.authPluginName !== undefined
) {
// Validate types to fail fast with clear errors
if (!Buffer.isBuffer(handshake.authToken)) {
throw new TypeError(
'HandshakeResponse authToken must be a Buffer when provided'
);
}
if (typeof handshake.authPluginName !== 'string') {
throw new TypeError(
'HandshakeResponse authPluginName must be a string when provided'
);
}
this.authToken = handshake.authToken;
this.authPluginName = handshake.authPluginName;
} else {
// Fallback to legacy behavior: calculate mysql_native_password token
// TODO: pre-4.1 auth support
let authToken;
if (this.passwordSha1) {
authToken = auth41.calculateTokenFromPasswordSha(
this.passwordSha1,
this.authPluginData1,
this.authPluginData2
);
} else {
authToken = auth41.calculateToken(
this.password,
this.authPluginData1,
this.authPluginData2
);
}
this.authToken = authToken;
this.authPluginName = 'mysql_native_password';
}
this.charsetNumber = handshake.charsetNumber;
this.encoding = CharsetToEncoding[handshake.charsetNumber];
this.connectAttributes = handshake.connectAttributes;
}
serializeResponse(buffer) {
const isSet = (flag) => this.clientFlags & ClientConstants[flag];
const packet = new Packet(0, buffer, 0, buffer.length);
packet.offset = 4;
packet.writeInt32(this.clientFlags);
packet.writeInt32(0); // max packet size. todo: move to config
packet.writeInt8(this.charsetNumber);
packet.skip(23);
const encoding = this.encoding;
packet.writeNullTerminatedString(this.user, encoding);
let k;
if (isSet('PLUGIN_AUTH_LENENC_CLIENT_DATA')) {
packet.writeLengthCodedNumber(this.authToken.length);
packet.writeBuffer(this.authToken);
} else if (isSet('SECURE_CONNECTION')) {
packet.writeInt8(this.authToken.length);
packet.writeBuffer(this.authToken);
} else {
packet.writeBuffer(this.authToken);
packet.writeInt8(0);
}
if (isSet('CONNECT_WITH_DB')) {
packet.writeNullTerminatedString(this.database, encoding);
}
if (isSet('PLUGIN_AUTH')) {
// Use the auth plugin name specified by the caller (optimized for server's preference)
// or fall back to mysql_native_password for backward compatibility
packet.writeNullTerminatedString(
this.authPluginName || 'mysql_native_password',
'latin1'
);
}
if (isSet('CONNECT_ATTRS')) {
const connectAttributes = this.connectAttributes || {};
const attrNames = Object.keys(connectAttributes);
let keysLength = 0;
for (k = 0; k < attrNames.length; ++k) {
keysLength += Packet.lengthCodedStringLength(attrNames[k], encoding);
keysLength += Packet.lengthCodedStringLength(
connectAttributes[attrNames[k]],
encoding
);
}
packet.writeLengthCodedNumber(keysLength);
for (k = 0; k < attrNames.length; ++k) {
packet.writeLengthCodedString(attrNames[k], encoding);
packet.writeLengthCodedString(
connectAttributes[attrNames[k]],
encoding
);
}
}
return packet;
}
toPacket() {
if (typeof this.user !== 'string') {
throw new Error('"user" connection config property must be a string');
}
if (typeof this.database !== 'string') {
throw new Error('"database" connection config property must be a string');
}
// dry run: calculate resulting packet length
const p = this.serializeResponse(Packet.MockBuffer());
return this.serializeResponse(Buffer.alloc(p.offset));
}
static fromPacket(packet, serverFlags = 0xffffffff) {
const args = {};
args.clientFlags = packet.readInt32();
function isSet(flag) {
return args.clientFlags & serverFlags & ClientConstants[flag];
}
args.maxPacketSize = packet.readInt32();
args.charsetNumber = packet.readInt8();
const encoding = CharsetToEncoding[args.charsetNumber];
args.encoding = encoding;
packet.skip(23);
args.user = packet.readNullTerminatedString(encoding);
let authTokenLength;
if (isSet('PLUGIN_AUTH_LENENC_CLIENT_DATA')) {
authTokenLength = packet.readLengthCodedNumber(encoding);
args.authToken = packet.readBuffer(authTokenLength);
} else if (isSet('SECURE_CONNECTION')) {
authTokenLength = packet.readInt8();
args.authToken = packet.readBuffer(authTokenLength);
} else {
args.authToken = packet.readNullTerminatedString(encoding);
}
if (isSet('CONNECT_WITH_DB')) {
args.database = packet.readNullTerminatedString(encoding);
}
if (isSet('PLUGIN_AUTH')) {
args.authPluginName = packet.readNullTerminatedString(encoding);
}
if (isSet('CONNECT_ATTRS')) {
const keysLength = packet.readLengthCodedNumber(encoding);
const keysEnd = packet.offset + keysLength;
const attrs = {};
while (packet.offset < keysEnd) {
attrs[packet.readLengthCodedString(encoding)] =
packet.readLengthCodedString(encoding);
}
args.connectAttributes = attrs;
}
return args;
}
}
module.exports = HandshakeResponse;
+154
View File
@@ -0,0 +1,154 @@
// This file was modified by Oracle on June 1, 2021.
// A utility method was introduced to generate an Error instance from a
// binary server packet.
// Modifications copyright (c) 2021, Oracle and/or its affiliates.
// This file was modified by Oracle on September 21, 2021.
// The new AuthNextFactor packet is now available.
// Modifications copyright (c) 2021, Oracle and/or its affiliates.
'use strict';
const process = require('process');
const AuthNextFactor = require('./auth_next_factor');
const AuthSwitchRequest = require('./auth_switch_request');
const AuthSwitchRequestMoreData = require('./auth_switch_request_more_data');
const AuthSwitchResponse = require('./auth_switch_response');
const BinaryRow = require('./binary_row');
const BinlogDump = require('./binlog_dump');
const ChangeUser = require('./change_user');
const CloseStatement = require('./close_statement');
const ColumnDefinition = require('./column_definition');
const Execute = require('./execute');
const Handshake = require('./handshake');
const HandshakeResponse = require('./handshake_response');
const PrepareStatement = require('./prepare_statement');
const PreparedStatementHeader = require('./prepared_statement_header');
const Query = require('./query');
const RegisterSlave = require('./register_slave');
const ResetConnection = require('./reset_connection');
const ResultSetHeader = require('./resultset_header');
const SSLRequest = require('./ssl_request');
const TextRow = require('./text_row');
const ctorMap = {
AuthNextFactor,
AuthSwitchRequest,
AuthSwitchRequestMoreData,
AuthSwitchResponse,
BinaryRow,
BinlogDump,
ChangeUser,
CloseStatement,
ColumnDefinition,
Execute,
Handshake,
HandshakeResponse,
PrepareStatement,
PreparedStatementHeader,
Query,
RegisterSlave,
ResetConnection,
ResultSetHeader,
SSLRequest,
TextRow,
};
Object.entries(ctorMap).forEach(([name, ctor]) => {
module.exports[name] = ctor;
// monkey-patch it to include name if debug is on
if (process.env.NODE_DEBUG) {
if (ctor.prototype.toPacket) {
const old = ctor.prototype.toPacket;
ctor.prototype.toPacket = function () {
const p = old.call(this);
p._name = name;
return p;
};
}
}
});
// simple packets:
const Packet = require('./packet');
exports.Packet = Packet;
class OK {
static toPacket(args, encoding) {
args = args || {};
const affectedRows = args.affectedRows || 0;
const insertId = args.insertId || 0;
const serverStatus = args.serverStatus || 0;
const warningCount = args.warningCount || 0;
const message = args.message || '';
let length = 9 + Packet.lengthCodedNumberLength(affectedRows);
length += Packet.lengthCodedNumberLength(insertId);
const buffer = Buffer.allocUnsafe(length);
const packet = new Packet(0, buffer, 0, length);
packet.offset = 4;
packet.writeInt8(0);
packet.writeLengthCodedNumber(affectedRows);
packet.writeLengthCodedNumber(insertId);
packet.writeInt16(serverStatus);
packet.writeInt16(warningCount);
packet.writeString(message, encoding);
packet._name = 'OK';
return packet;
}
}
exports.OK = OK;
// warnings, statusFlags
class EOF {
static toPacket(warnings, statusFlags) {
if (typeof warnings === 'undefined') {
warnings = 0;
}
if (typeof statusFlags === 'undefined') {
statusFlags = 0;
}
const packet = new Packet(0, Buffer.allocUnsafe(9), 0, 9);
packet.offset = 4;
packet.writeInt8(0xfe);
packet.writeInt16(warnings);
packet.writeInt16(statusFlags);
packet._name = 'EOF';
return packet;
}
}
exports.EOF = EOF;
class Error {
static toPacket(args, encoding) {
const length = 13 + Buffer.byteLength(args.message, 'utf8');
const packet = new Packet(0, Buffer.allocUnsafe(length), 0, length);
packet.offset = 4;
packet.writeInt8(0xff);
packet.writeInt16(args.code);
// TODO: sql state parameter
packet.writeString('#_____', encoding);
packet.writeString(args.message, encoding);
packet._name = 'Error';
return packet;
}
static fromPacket(packet) {
packet.readInt8(); // marker
const code = packet.readInt16();
packet.readString(1, 'ascii'); // sql state marker
// The SQL state of the ERR_Packet which is always 5 bytes long.
// https://dev.mysql.com/doc/dev/mysql-server/8.0.11/page_protocol_basic_dt_strings.html#sect_protocol_basic_dt_string_fix
packet.readString(5, 'ascii'); // sql state (ignore for now)
const message = packet.readNullTerminatedString('utf8');
const error = new Error();
error.message = message;
error.code = code;
return error;
}
}
exports.Error = Error;
+981
View File
@@ -0,0 +1,981 @@
// This file was modified by Oracle on June 1, 2021.
// A comment describing some changes in the strict default SQL mode regarding
// non-standard dates was introduced.
// Modifications copyright (c) 2021, Oracle and/or its affiliates.
'use strict';
const ErrorCodeToName = require('../constants/errors.js');
const NativeBuffer = require('buffer').Buffer;
const Long = require('long');
const StringParser = require('../parsers/string.js');
const Types = require('../constants/types.js');
const INVALID_DATE = new Date(NaN);
// this is nearly duplicate of previous function so generated code is not slower
// due to "if (dateStrings)" branching
const pad = '000000000000';
function leftPad(num, value) {
const s = value.toString();
// if we don't need to pad
if (s.length >= num) {
return s;
}
return (pad + s).slice(-num);
}
// The whole reason parse* function below exist
// is because String creation is relatively expensive (at least with V8), and if we have
// a buffer with "12345" content ideally we would like to bypass intermediate
// "12345" string creation and directly build 12345 number out of
// <Buffer 31 32 33 34 35> data.
// In my benchmarks the difference is ~25M 8-digit numbers per second vs
// 4.5 M using Number(packet.readLengthCodedString())
// not used when size is close to max precision as series of *10 accumulate error
// and approximate result mihgt be diffreent from (approximate as well) Number(bigNumStringValue))
// In the futire node version if speed difference is smaller parse* functions might be removed
// don't consider them as Packet public API
const minus = '-'.charCodeAt(0);
const plus = '+'.charCodeAt(0);
// TODO: handle E notation
const dot = '.'.charCodeAt(0);
const exponent = 'e'.charCodeAt(0);
const exponentCapital = 'E'.charCodeAt(0);
class Packet {
constructor(id, buffer, start, end) {
// hot path, enable checks when testing only
// if (!Buffer.isBuffer(buffer) || typeof start == 'undefined' || typeof end == 'undefined')
// throw new Error('invalid packet');
this.sequenceId = id;
this.numPackets = 1;
this.buffer = buffer;
this.start = start;
this.offset = start + 4;
this.end = end;
}
// ==============================
// readers
// ==============================
reset() {
this.offset = this.start + 4;
}
length() {
return this.end - this.start;
}
slice() {
return this.buffer.slice(this.start, this.end);
}
dump() {
console.log(
[this.buffer.asciiSlice(this.start, this.end)],
this.buffer.slice(this.start, this.end),
this.length(),
this.sequenceId
);
}
haveMoreData() {
return this.end > this.offset;
}
skip(num) {
this.offset += num;
}
readInt8() {
return this.buffer[this.offset++];
}
readInt16() {
this.offset += 2;
return this.buffer.readUInt16LE(this.offset - 2);
}
readInt24() {
return this.readInt16() + (this.readInt8() << 16);
}
readInt32() {
this.offset += 4;
return this.buffer.readUInt32LE(this.offset - 4);
}
readSInt8() {
return this.buffer.readInt8(this.offset++);
}
readSInt16() {
this.offset += 2;
return this.buffer.readInt16LE(this.offset - 2);
}
readSInt32() {
this.offset += 4;
return this.buffer.readInt32LE(this.offset - 4);
}
readInt64JSNumber() {
const word0 = this.readInt32();
const word1 = this.readInt32();
const l = new Long(word0, word1, true);
return l.toNumber();
}
readSInt64JSNumber() {
const word0 = this.readInt32();
const word1 = this.readInt32();
if (!(word1 & 0x80000000)) {
return word0 + 0x100000000 * word1;
}
const l = new Long(word0, word1, false);
return l.toNumber();
}
readInt64String() {
const word0 = this.readInt32();
const word1 = this.readInt32();
const res = new Long(word0, word1, true);
return res.toString();
}
readSInt64String() {
const word0 = this.readInt32();
const word1 = this.readInt32();
const res = new Long(word0, word1, false);
return res.toString();
}
readInt64() {
const word0 = this.readInt32();
const word1 = this.readInt32();
const res = new Long(word0, word1, true);
const resNumber = res.toNumber();
return Number.isSafeInteger(resNumber) ? resNumber : res.toString();
}
readSInt64() {
const word0 = this.readInt32();
const word1 = this.readInt32();
const res = new Long(word0, word1, false);
const resNumber = res.toNumber();
return Number.isSafeInteger(resNumber) ? resNumber : res.toString();
}
isEOF() {
return this.buffer[this.offset] === 0xfe && this.length() < 13;
}
eofStatusFlags() {
return this.buffer.readInt16LE(this.offset + 3);
}
eofWarningCount() {
return this.buffer.readInt16LE(this.offset + 1);
}
readLengthCodedNumber(bigNumberStrings, signed) {
const byte1 = this.buffer[this.offset++];
if (byte1 < 251) {
return byte1;
}
return this.readLengthCodedNumberExt(byte1, bigNumberStrings, signed);
}
readLengthCodedNumberSigned(bigNumberStrings) {
return this.readLengthCodedNumber(bigNumberStrings, true);
}
readLengthCodedNumberExt(tag, bigNumberStrings, signed) {
let word0, word1;
let res;
if (tag === 0xfb) {
return null;
}
if (tag === 0xfc) {
return this.readInt8() + (this.readInt8() << 8);
}
if (tag === 0xfd) {
return this.readInt8() + (this.readInt8() << 8) + (this.readInt8() << 16);
}
if (tag === 0xfe) {
// TODO: check version
// Up to MySQL 3.22, 0xfe was followed by a 4-byte integer.
word0 = this.readInt32();
word1 = this.readInt32();
if (word1 === 0) {
return word0; // don't convert to float if possible
}
if (word1 < 2097152) {
// max exact float point int, 2^52 / 2^32
return word1 * 0x100000000 + word0;
}
res = new Long(word0, word1, !signed); // Long need unsigned
const resNumber = res.toNumber();
const resString = res.toString();
if (bigNumberStrings || !Number.isSafeInteger(resNumber)) {
return resString;
}
return resNumber;
}
console.trace();
throw new Error(`Should not reach here: ${tag}`);
}
readFloat() {
const res = this.buffer.readFloatLE(this.offset);
this.offset += 4;
return res;
}
readDouble() {
const res = this.buffer.readDoubleLE(this.offset);
this.offset += 8;
return res;
}
readBuffer(len) {
if (typeof len === 'undefined') {
len = this.end - this.offset;
}
this.offset += len;
return this.buffer.slice(this.offset - len, this.offset);
}
// DATE, DATETIME and TIMESTAMP
readDateTime(timezone) {
if (!timezone || timezone === 'Z' || timezone === 'local') {
const length = this.readInt8();
if (length === 0xfb) {
return null;
}
let y = 0;
let m = 0;
let d = 0;
let H = 0;
let M = 0;
let S = 0;
let ms = 0;
if (length > 3) {
y = this.readInt16();
m = this.readInt8();
d = this.readInt8();
}
if (length > 6) {
H = this.readInt8();
M = this.readInt8();
S = this.readInt8();
}
if (length > 10) {
ms = this.readInt32() / 1000;
}
// NO_ZERO_DATE mode and NO_ZERO_IN_DATE mode are part of the strict
// default SQL mode used by MySQL 8.0. This means that non-standard
// dates like '0000-00-00' become NULL. For older versions and other
// possible MySQL flavours we still need to account for the
// non-standard behaviour.
if (y + m + d + H + M + S + ms === 0) {
return INVALID_DATE;
}
if (timezone === 'Z') {
return new Date(Date.UTC(y, m - 1, d, H, M, S, ms));
}
return new Date(y, m - 1, d, H, M, S, ms);
}
let str = this.readDateTimeString(6, 'T', null);
if (!str) {
return INVALID_DATE;
}
if (str.length === 10) {
str += 'T00:00:00';
}
return new Date(str + timezone);
}
readDateTimeString(decimals, timeSep, columnType) {
const length = this.readInt8();
let y = 0;
let m = 0;
let d = 0;
let H = 0;
let M = 0;
let S = 0;
let ms = 0;
let str;
if (length > 3) {
y = this.readInt16();
m = this.readInt8();
d = this.readInt8();
str = [leftPad(4, y), leftPad(2, m), leftPad(2, d)].join('-');
}
if (length > 6) {
H = this.readInt8();
M = this.readInt8();
S = this.readInt8();
str += `${timeSep || ' '}${[
leftPad(2, H),
leftPad(2, M),
leftPad(2, S),
].join(':')}`;
} else if (
columnType === Types.DATETIME ||
columnType === Types.TIMESTAMP
) {
str += ' 00:00:00';
}
if (length > 10) {
ms = this.readInt32();
str += '.';
if (decimals) {
ms = leftPad(6, ms);
if (ms.length > decimals) {
ms = ms.substring(0, decimals); // rounding is done at the MySQL side, only 0 are here
}
}
str += ms;
}
return str;
}
// TIME - value as a string, Can be negative
readTimeString(convertTtoMs) {
const length = this.readInt8();
if (length === 0) {
return '00:00:00';
}
const sign = this.readInt8() ? -1 : 1; // 'isNegative' flag byte
let d = 0;
let H = 0;
let M = 0;
let S = 0;
let ms = 0;
if (length > 6) {
d = this.readInt32();
H = this.readInt8();
M = this.readInt8();
S = this.readInt8();
}
if (length > 10) {
ms = this.readInt32();
}
if (convertTtoMs) {
H += d * 24;
M += H * 60;
S += M * 60;
ms += S * 1000;
ms *= sign;
return ms;
}
// Format follows mySQL TIME format ([-][h]hh:mm:ss[.u[u[u[u[u[u]]]]]])
// For positive times below 24 hours, this makes it equal to ISO 8601 times
return (
(sign === -1 ? '-' : '') +
[leftPad(2, d * 24 + H), leftPad(2, M), leftPad(2, S)].join(':') +
(ms ? `.${ms}`.replace(/0+$/, '') : '')
);
}
readLengthCodedString(encoding) {
const len = this.readLengthCodedNumber();
// TODO: check manually first byte here to avoid polymorphic return type?
if (len === null) {
return null;
}
this.offset += len;
// TODO: Use characterSetCode to get proper encoding
// https://github.com/sidorares/node-mysql2/pull/374
return StringParser.decode(
this.buffer,
encoding,
this.offset - len,
this.offset
);
}
readLengthCodedBuffer() {
const len = this.readLengthCodedNumber();
if (len === null) {
return null;
}
return this.readBuffer(len);
}
readNullTerminatedString(encoding) {
const start = this.offset;
let end = this.offset;
while (end < this.end && this.buffer[end] !== 0x00) {
end = end + 1;
}
this.offset = end + 1;
return StringParser.decode(this.buffer, encoding, start, end);
}
// TODO reuse?
readString(len, encoding) {
if (typeof len === 'string' && typeof encoding === 'undefined') {
encoding = len;
len = undefined;
}
if (typeof len === 'undefined') {
len = this.end - this.offset;
}
this.offset += len;
return StringParser.decode(
this.buffer,
encoding,
this.offset - len,
this.offset
);
}
parseInt(len, supportBigNumbers) {
if (len === null) {
return null;
}
if (len >= 14 && !supportBigNumbers) {
const s = this.buffer.toString('ascii', this.offset, this.offset + len);
this.offset += len;
return Number(s);
}
let result = 0;
const start = this.offset;
const end = this.offset + len;
let sign = 1;
if (len === 0) {
return 0; // TODO: assert? exception?
}
if (this.buffer[this.offset] === minus) {
this.offset++;
sign = -1;
}
// max precise int is 9007199254740992
let str;
const numDigits = end - this.offset;
if (supportBigNumbers) {
if (numDigits >= 15) {
str = this.readString(end - this.offset, 'binary');
result = parseInt(str, 10);
if (Number.isSafeInteger(sign * result)) {
return sign * result;
}
return sign === -1 ? `-${str}` : str;
}
if (numDigits > 16) {
str = this.readString(end - this.offset);
return sign === -1 ? `-${str}` : str;
}
}
if (this.buffer[this.offset] === plus) {
this.offset++; // just ignore
}
while (this.offset < end) {
result *= 10;
result += this.buffer[this.offset] - 48;
this.offset++;
}
const num = result * sign;
if (!supportBigNumbers) {
return num;
}
if (Number.isSafeInteger(num)) {
return num;
}
return this.buffer.toString('ascii', start, end);
}
// note that if value of inputNumberAsString is bigger than MAX_SAFE_INTEGER
// ( or smaller than MIN_SAFE_INTEGER ) the parseIntNoBigCheck result might be
// different from what you would get from Number(inputNumberAsString)
// String(parseIntNoBigCheck) <> String(Number(inputNumberAsString)) <> inputNumberAsString
parseIntNoBigCheck(len) {
if (len === null) {
return null;
}
let result = 0;
const end = this.offset + len;
let sign = 1;
if (len === 0) {
return 0; // TODO: assert? exception?
}
if (this.buffer[this.offset] === minus) {
this.offset++;
sign = -1;
}
if (this.buffer[this.offset] === plus) {
this.offset++; // just ignore
}
while (this.offset < end) {
result *= 10;
result += this.buffer[this.offset] - 48;
this.offset++;
}
return result * sign;
}
// adapted from https://github.com/mysqljs/mysql/blob/dc9c152a87ec51a1f647447268917243d2eab1fd/lib/protocol/Parser.js
parseGeometryValue() {
const buffer = this.readLengthCodedBuffer();
let offset = 4;
if (buffer === null || !buffer.length) {
return null;
}
const bufferLength = buffer.length;
function parseGeometry() {
let x, y, i, j, numPoints, numRings, num, line;
let result = null;
if (offset + 5 > bufferLength) {
return null;
}
const byteOrder = buffer.readUInt8(offset);
offset += 1;
const wkbType = byteOrder
? buffer.readUInt32LE(offset)
: buffer.readUInt32BE(offset);
offset += 4;
switch (wkbType) {
case 1: // WKBPoint
if (offset + 16 > bufferLength) {
return null;
}
x = byteOrder
? buffer.readDoubleLE(offset)
: buffer.readDoubleBE(offset);
offset += 8;
y = byteOrder
? buffer.readDoubleLE(offset)
: buffer.readDoubleBE(offset);
offset += 8;
result = { x: x, y: y };
break;
case 2: // WKBLineString
if (offset + 4 > bufferLength) {
return null;
}
numPoints = byteOrder
? buffer.readUInt32LE(offset)
: buffer.readUInt32BE(offset);
offset += 4;
if (numPoints > (bufferLength - offset) / 16) {
return null;
}
result = [];
for (i = numPoints; i > 0; i--) {
if (offset + 16 > bufferLength) {
break;
}
x = byteOrder
? buffer.readDoubleLE(offset)
: buffer.readDoubleBE(offset);
offset += 8;
y = byteOrder
? buffer.readDoubleLE(offset)
: buffer.readDoubleBE(offset);
offset += 8;
result.push({ x: x, y: y });
}
break;
case 3: // WKBPolygon
if (offset + 4 > bufferLength) {
return null;
}
numRings = byteOrder
? buffer.readUInt32LE(offset)
: buffer.readUInt32BE(offset);
offset += 4;
if (numRings > (bufferLength - offset) / 4) {
return null;
}
result = [];
for (i = numRings; i > 0; i--) {
if (offset + 4 > bufferLength) {
break;
}
numPoints = byteOrder
? buffer.readUInt32LE(offset)
: buffer.readUInt32BE(offset);
offset += 4;
line = [];
for (j = numPoints; j > 0; j--) {
if (offset + 16 > bufferLength) {
break;
}
x = byteOrder
? buffer.readDoubleLE(offset)
: buffer.readDoubleBE(offset);
offset += 8;
y = byteOrder
? buffer.readDoubleLE(offset)
: buffer.readDoubleBE(offset);
offset += 8;
line.push({ x: x, y: y });
}
result.push(line);
}
break;
case 4: // WKBMultiPoint
case 5: // WKBMultiLineString
case 6: // WKBMultiPolygon
case 7: // WKBGeometryCollection
if (offset + 4 > bufferLength) {
return null;
}
num = byteOrder
? buffer.readUInt32LE(offset)
: buffer.readUInt32BE(offset);
offset += 4;
if (num > (bufferLength - offset) / 9) {
return null;
}
result = [];
for (i = num; i > 0; i--) {
result.push(parseGeometry());
}
break;
}
return result;
}
return parseGeometry();
}
parseVector() {
const bufLen = this.readLengthCodedNumber();
const vectorEnd = this.offset + bufLen;
const result = [];
while (this.offset < vectorEnd && this.offset < this.end) {
result.push(this.readFloat());
}
return result;
}
parseDate(timezone) {
const strLen = this.readLengthCodedNumber();
if (strLen === null) {
return null;
}
if (strLen !== 10) {
// we expect only YYYY-MM-DD here.
// if for some reason it's not the case return invalid date
return new Date(NaN);
}
const y = this.parseInt(4);
this.offset++; // -
const m = this.parseInt(2);
this.offset++; // -
const d = this.parseInt(2);
if (!timezone || timezone === 'local') {
return new Date(y, m - 1, d);
}
if (timezone === 'Z') {
return new Date(Date.UTC(y, m - 1, d));
}
return new Date(
`${leftPad(4, y)}-${leftPad(2, m)}-${leftPad(2, d)}T00:00:00${timezone}`
);
}
parseDateTime(timezone) {
const str = this.readLengthCodedString('binary');
if (str === null) {
return null;
}
if (!timezone || timezone === 'local') {
return new Date(str);
}
return new Date(`${str}${timezone}`);
}
parseFloat(len) {
if (len === null) {
return null;
}
if (len === 0) {
return 0; // TODO: assert? exception?
}
// For numbers with many digits (>17), use built-in parseFloat to avoid
// precision loss from accumulated rounding errors in repeated *10 operations.
// This fixes issues #2928 (MAX_VALUE doubles) and #3690 (DECIMAL(36,18))
// where very large numbers or numbers with many fractional digits lose precision.
// The threshold of 17 is based on IEEE 754 double precision (~15-17 significant digits).
// Testing shows minimal performance impact as most real-world numbers are shorter.
if (len > 17) {
const str = this.buffer.toString('utf8', this.offset, this.offset + len);
this.offset += len;
return Number.parseFloat(str);
}
let result = 0;
const end = this.offset + len;
let factor = 1;
let pastDot = false;
let charCode = 0;
if (this.buffer[this.offset] === minus) {
this.offset++;
factor = -1;
}
if (this.buffer[this.offset] === plus) {
this.offset++; // just ignore
}
while (this.offset < end) {
charCode = this.buffer[this.offset];
if (charCode === dot) {
pastDot = true;
this.offset++;
} else if (charCode === exponent || charCode === exponentCapital) {
// Scientific notation detected - bail out to parseFloat for exact match.
// Manual calculation with Math.pow(10, exp) cannot match parseFloat()
// exactly for most non-zero exponents due to accumulated rounding errors.
const start = end - len;
const str = this.buffer.toString('utf8', start, end);
this.offset = end;
return Number.parseFloat(str);
} else {
result *= 10;
result += this.buffer[this.offset] - 48;
this.offset++;
if (pastDot) {
factor = factor * 10;
}
}
}
return result / factor;
}
parseLengthCodedIntNoBigCheck() {
return this.parseIntNoBigCheck(this.readLengthCodedNumber());
}
parseLengthCodedInt(supportBigNumbers) {
return this.parseInt(this.readLengthCodedNumber(), supportBigNumbers);
}
parseLengthCodedIntString() {
return this.readLengthCodedString('binary');
}
parseLengthCodedFloat() {
return this.parseFloat(this.readLengthCodedNumber());
}
peekByte() {
return this.buffer[this.offset];
}
// OxFE is often used as "Alt" flag - not ok, not error.
// For example, it's first byte of AuthSwitchRequest
isAlt() {
return this.peekByte() === 0xfe;
}
isError() {
return this.peekByte() === 0xff;
}
asError(encoding) {
this.reset();
this.readInt8(); // fieldCount
const errorCode = this.readInt16();
let sqlState = '';
if (this.buffer[this.offset] === 0x23) {
this.skip(1);
sqlState = this.readBuffer(5).toString();
}
const message = this.readString(undefined, encoding);
const err = new Error(message);
err.code = ErrorCodeToName[errorCode];
err.errno = errorCode;
err.sqlState = sqlState;
err.sqlMessage = message;
return err;
}
writeInt32(n) {
this.buffer.writeUInt32LE(n, this.offset);
this.offset += 4;
}
writeInt24(n) {
this.writeInt8(n & 0xff);
this.writeInt16(n >> 8);
}
writeInt16(n) {
this.buffer.writeUInt16LE(n, this.offset);
this.offset += 2;
}
writeInt8(n) {
this.buffer.writeUInt8(n, this.offset);
this.offset++;
}
writeDouble(n) {
this.buffer.writeDoubleLE(n, this.offset);
this.offset += 8;
}
writeBuffer(b) {
b.copy(this.buffer, this.offset);
this.offset += b.length;
}
writeNull() {
this.buffer[this.offset] = 0xfb;
this.offset++;
}
// TODO: refactor following three?
writeNullTerminatedString(s, encoding) {
const buf = StringParser.encode(s, encoding);
this.buffer.length && buf.copy(this.buffer, this.offset);
this.offset += buf.length;
this.writeInt8(0);
}
writeString(s, encoding) {
if (s === null) {
this.writeInt8(0xfb);
return;
}
if (s.length === 0) {
return;
}
// const bytes = Buffer.byteLength(s, 'utf8');
// this.buffer.write(s, this.offset, bytes, 'utf8');
// this.offset += bytes;
const buf = StringParser.encode(s, encoding);
this.buffer.length && buf.copy(this.buffer, this.offset);
this.offset += buf.length;
}
writeLengthCodedString(s, encoding) {
const buf = StringParser.encode(s, encoding);
this.writeLengthCodedNumber(buf.length);
this.buffer.length && buf.copy(this.buffer, this.offset);
this.offset += buf.length;
}
writeLengthCodedBuffer(b) {
this.writeLengthCodedNumber(b.length);
b.copy(this.buffer, this.offset);
this.offset += b.length;
}
writeLengthCodedNumber(n) {
if (n < 0xfb) {
return this.writeInt8(n);
}
if (n < 0xffff) {
this.writeInt8(0xfc);
return this.writeInt16(n);
}
if (n < 0xffffff) {
this.writeInt8(0xfd);
return this.writeInt24(n);
}
if (n === null) {
return this.writeInt8(0xfb);
}
this.writeInt8(0xfe);
this.buffer.writeUInt32LE(n >>> 0, this.offset);
this.offset += 4;
this.buffer.writeUInt32LE(Math.floor(n / 0x100000000), this.offset);
this.offset += 4;
return this.offset;
}
writeDate(d, timezone) {
this.buffer.writeUInt8(11, this.offset);
if (!timezone || timezone === 'local') {
this.buffer.writeUInt16LE(d.getFullYear(), this.offset + 1);
this.buffer.writeUInt8(d.getMonth() + 1, this.offset + 3);
this.buffer.writeUInt8(d.getDate(), this.offset + 4);
this.buffer.writeUInt8(d.getHours(), this.offset + 5);
this.buffer.writeUInt8(d.getMinutes(), this.offset + 6);
this.buffer.writeUInt8(d.getSeconds(), this.offset + 7);
this.buffer.writeUInt32LE(d.getMilliseconds() * 1000, this.offset + 8);
} else {
if (timezone !== 'Z') {
const offset =
(timezone[0] === '-' ? -1 : 1) *
(parseInt(timezone.substring(1, 3), 10) * 60 +
parseInt(timezone.substring(4), 10));
if (offset !== 0) {
d = new Date(d.getTime() + 60000 * offset);
}
}
this.buffer.writeUInt16LE(d.getUTCFullYear(), this.offset + 1);
this.buffer.writeUInt8(d.getUTCMonth() + 1, this.offset + 3);
this.buffer.writeUInt8(d.getUTCDate(), this.offset + 4);
this.buffer.writeUInt8(d.getUTCHours(), this.offset + 5);
this.buffer.writeUInt8(d.getUTCMinutes(), this.offset + 6);
this.buffer.writeUInt8(d.getUTCSeconds(), this.offset + 7);
this.buffer.writeUInt32LE(d.getUTCMilliseconds() * 1000, this.offset + 8);
}
this.offset += 12;
}
writeHeader(sequenceId) {
const offset = this.offset;
this.offset = 0;
this.writeInt24(this.buffer.length - 4);
this.writeInt8(sequenceId);
this.offset = offset;
}
clone() {
return new Packet(this.sequenceId, this.buffer, this.start, this.end);
}
type() {
if (this.isEOF()) {
return 'EOF';
}
if (this.isError()) {
return 'Error';
}
if (this.buffer[this.offset] === 0) {
return 'maybeOK'; // could be other packet types as well
}
return '';
}
static lengthCodedNumberLength(n) {
if (n < 0xfb) {
return 1;
}
if (n < 0xffff) {
return 3;
}
if (n < 0xffffff) {
return 5;
}
return 9;
}
static lengthCodedStringLength(str, encoding) {
const buf = StringParser.encode(str, encoding);
const slen = buf.length;
return Packet.lengthCodedNumberLength(slen) + slen;
}
static MockBuffer() {
const noop = function () {};
const res = Buffer.alloc(0);
for (const op in NativeBuffer.prototype) {
if (typeof res[op] === 'function') {
res[op] = noop;
}
}
return res;
}
}
module.exports = Packet;
+27
View File
@@ -0,0 +1,27 @@
'use strict';
const Packet = require('../packets/packet');
const CommandCodes = require('../constants/commands');
const StringParser = require('../parsers/string.js');
const CharsetToEncoding = require('../constants/charset_encodings.js');
class PrepareStatement {
constructor(sql, charsetNumber) {
this.query = sql;
this.charsetNumber = charsetNumber;
this.encoding = CharsetToEncoding[charsetNumber];
}
toPacket() {
const buf = StringParser.encode(this.query, this.encoding);
const length = 5 + buf.length;
const buffer = Buffer.allocUnsafe(length);
const packet = new Packet(0, buffer, 0, length);
packet.offset = 4;
packet.writeInt8(CommandCodes.STMT_PREPARE);
packet.writeBuffer(buf);
return packet;
}
}
module.exports = PrepareStatement;
+16
View File
@@ -0,0 +1,16 @@
'use strict';
class PreparedStatementHeader {
constructor(packet) {
packet.skip(1); // should be 0
this.id = packet.readInt32();
this.fieldCount = packet.readInt16();
this.parameterCount = packet.readInt16();
packet.skip(1); // should be 0
this.warningCount = packet.readInt16();
}
}
// TODO: toPacket
module.exports = PreparedStatementHeader;
+102
View File
@@ -0,0 +1,102 @@
'use strict';
const Packet = require('../packets/packet.js');
const CommandCode = require('../constants/commands.js');
const StringParser = require('../parsers/string.js');
const CharsetToEncoding = require('../constants/charset_encodings.js');
const ClientConstants = require('../constants/client.js');
const Types = require('../constants/types.js');
const { toParameter } = require('./encode_parameter.js');
class Query {
constructor(sql, charsetNumber, attributes, clientFlags) {
this.query = sql;
this.charsetNumber = charsetNumber;
this.encoding = CharsetToEncoding[charsetNumber];
this.attributes = attributes;
this.clientFlags = clientFlags || 0;
}
serializeToBuffer(buffer) {
const useQueryAttributes =
this.clientFlags & ClientConstants.CLIENT_QUERY_ATTRIBUTES;
const sqlBuf = StringParser.encode(this.query, this.encoding);
const packet = new Packet(0, buffer, 0, buffer.length);
packet.offset = 4;
packet.writeInt8(CommandCode.QUERY);
if (useQueryAttributes) {
const attrs = this.attributes;
const names = attrs ? Object.keys(attrs) : [];
const paramCount = names.length;
packet.writeLengthCodedNumber(paramCount);
packet.writeLengthCodedNumber(1); // parameter_set_count, always 1
if (paramCount > 0) {
const parameters = names.map((name) =>
toParameter(attrs[name], this.encoding, 'local')
);
// null bitmap
let bitmap = 0;
let bitValue = 1;
parameters.forEach((parameter) => {
if (parameter.type === Types.NULL) {
bitmap += bitValue;
}
bitValue *= 2;
if (bitValue === 256) {
packet.writeInt8(bitmap);
bitmap = 0;
bitValue = 1;
}
});
if (bitValue !== 1) {
packet.writeInt8(bitmap);
}
packet.writeInt8(1); // new_params_bind_flag
// types and names
for (let i = 0; i < paramCount; i++) {
packet.writeInt8(parameters[i].type);
packet.writeInt8(0); // unsigned flag
packet.writeLengthCodedString(names[i], this.encoding);
}
// values
parameters.forEach((parameter) => {
if (parameter.type !== Types.NULL) {
parameter.writer.call(packet, parameter.value);
}
});
}
}
packet.writeBuffer(sqlBuf);
return packet;
}
toPacket() {
const useQueryAttributes =
this.clientFlags & ClientConstants.CLIENT_QUERY_ATTRIBUTES;
if (!useQueryAttributes) {
const buf = StringParser.encode(this.query, this.encoding);
const length = 5 + buf.length;
const buffer = Buffer.allocUnsafe(length);
const packet = new Packet(0, buffer, 0, length);
packet.offset = 4;
packet.writeInt8(CommandCode.QUERY);
packet.writeBuffer(buf);
return packet;
}
// dry run to calculate required buffer length
const p = this.serializeToBuffer(Packet.MockBuffer());
return this.serializeToBuffer(Buffer.allocUnsafe(p.offset));
}
}
module.exports = Query;
+46
View File
@@ -0,0 +1,46 @@
'use strict';
// http://dev.mysql.com/doc/internals/en/com-register-slave.html
// note that documentation is incorrect, for example command code is actually 0x15 but documented as 0x14
const Packet = require('../packets/packet');
const CommandCodes = require('../constants/commands');
class RegisterSlave {
constructor(opts) {
this.serverId = opts.serverId || 0;
this.slaveHostname = opts.slaveHostname || '';
this.slaveUser = opts.slaveUser || '';
this.slavePassword = opts.slavePassword || '';
this.slavePort = opts.slavePort || 0;
this.replicationRank = opts.replicationRank || 0;
this.masterId = opts.masterId || 0;
}
toPacket() {
const length =
15 + // TODO: should be ascii?
Buffer.byteLength(this.slaveHostname, 'utf8') +
Buffer.byteLength(this.slaveUser, 'utf8') +
Buffer.byteLength(this.slavePassword, 'utf8') +
3 +
4;
const buffer = Buffer.allocUnsafe(length);
const packet = new Packet(0, buffer, 0, length);
packet.offset = 4;
packet.writeInt8(CommandCodes.REGISTER_SLAVE);
packet.writeInt32(this.serverId);
packet.writeInt8(Buffer.byteLength(this.slaveHostname, 'utf8'));
packet.writeString(this.slaveHostname);
packet.writeInt8(Buffer.byteLength(this.slaveUser, 'utf8'));
packet.writeString(this.slaveUser);
packet.writeInt8(Buffer.byteLength(this.slavePassword, 'utf8'));
packet.writeString(this.slavePassword);
packet.writeInt16(this.slavePort);
packet.writeInt32(this.replicationRank);
packet.writeInt32(this.masterId);
return packet;
}
}
module.exports = RegisterSlave;
+17
View File
@@ -0,0 +1,17 @@
'use strict';
const Packet = require('../packets/packet');
const CommandCodes = require('../constants/commands');
class ResetConnection {
constructor() {}
toPacket() {
const packet = new Packet(0, Buffer.alloc(5), 0, 5);
packet.offset = 4;
packet.writeInt8(CommandCodes.RESET_CONNECTION);
return packet;
}
}
module.exports = ResetConnection;
+124
View File
@@ -0,0 +1,124 @@
'use strict';
// TODO: rename to OK packet
// https://dev.mysql.com/doc/internals/en/packet-OK_Packet.html
const Packet = require('./packet.js');
const ClientConstants = require('../constants/client.js');
const ServerSatusFlags = require('../constants/server_status.js');
const EncodingToCharset = require('../constants/encoding_charset.js');
const sessionInfoTypes = require('../constants/session_track.js');
class ResultSetHeader {
constructor(packet, connection) {
const bigNumberStrings = connection.config.bigNumberStrings;
const encoding = connection.serverEncoding;
const flags = connection._handshakePacket.capabilityFlags;
const isSet = function (flag) {
return flags & ClientConstants[flag];
};
if (packet.buffer[packet.offset] !== 0) {
this.fieldCount = packet.readLengthCodedNumber();
if (this.fieldCount === null) {
this.infileName = packet.readString(undefined, encoding);
}
return;
}
this.fieldCount = packet.readInt8(); // skip OK byte
this.affectedRows = packet.readLengthCodedNumber(bigNumberStrings);
this.insertId = packet.readLengthCodedNumberSigned(bigNumberStrings);
this.info = '';
if (isSet('PROTOCOL_41')) {
this.serverStatus = packet.readInt16();
this.warningStatus = packet.readInt16();
} else if (isSet('TRANSACTIONS')) {
this.serverStatus = packet.readInt16();
}
let stateChanges = null;
if (isSet('SESSION_TRACK') && packet.offset < packet.end) {
this.info = packet.readLengthCodedString(encoding);
if (this.serverStatus & ServerSatusFlags.SERVER_SESSION_STATE_CHANGED) {
// session change info record - see
// https://dev.mysql.com/doc/internals/en/packet-OK_Packet.html#cs-sect-packet-ok-sessioninfo
let len =
packet.offset < packet.end ? packet.readLengthCodedNumber() : 0;
const end = packet.offset + len;
let type, key, stateEnd;
if (len > 0) {
stateChanges = {
systemVariables: {},
schema: null,
gtids: [],
trackStateChange: null,
};
}
while (packet.offset < end) {
type = packet.readInt8();
len = packet.readLengthCodedNumber();
stateEnd = packet.offset + len;
if (type === sessionInfoTypes.SYSTEM_VARIABLES) {
key = packet.readLengthCodedString(encoding);
const val = packet.readLengthCodedString(encoding);
stateChanges.systemVariables[key] = val;
if (key === 'character_set_client') {
const charsetNumber = EncodingToCharset[val];
// TODO - better api for driver users to handle unknown encodings?
// maybe custom coverter in the config?
// For now just ignore character_set_client command if there is
// no known mapping from reported encoding to a charset code
if (typeof charsetNumber !== 'undefined') {
connection.config.charsetNumber = charsetNumber;
}
}
} else if (type === sessionInfoTypes.SCHEMA) {
key = packet.readLengthCodedString(encoding);
stateChanges.schema = key;
} else if (type === sessionInfoTypes.STATE_CHANGE) {
stateChanges.trackStateChange =
packet.readLengthCodedString(encoding);
} else if (type === sessionInfoTypes.STATE_GTIDS) {
// TODO: find if the first length coded string means anything. Usually comes as empty
// eslint-disable-next-line no-unused-vars
const _unknownString = packet.readLengthCodedString(encoding);
const gtid = packet.readLengthCodedString(encoding);
stateChanges.gtids = gtid.split(',');
} else {
// unsupported session track type. For now just ignore
}
packet.offset = stateEnd;
}
}
} else {
this.info = packet.readString(undefined, encoding);
}
if (stateChanges) {
this.stateChanges = stateChanges;
}
const m = this.info.match(/\schanged:\s*(\d+)/i);
if (m !== null) {
this.changedRows = parseInt(m[1], 10);
} else {
this.changedRows = 0;
}
}
// TODO: should be consistent instance member, but it's just easier here to have just function
static toPacket(fieldCount, insertId) {
let length = 4 + Packet.lengthCodedNumberLength(fieldCount);
if (typeof insertId !== 'undefined') {
length += Packet.lengthCodedNumberLength(insertId);
}
const buffer = Buffer.allocUnsafe(length);
const packet = new Packet(0, buffer, 0, length);
packet.offset = 4;
packet.writeLengthCodedNumber(fieldCount);
if (typeof insertId !== 'undefined') {
packet.writeLengthCodedNumber(insertId);
}
return packet;
}
}
module.exports = ResultSetHeader;
+25
View File
@@ -0,0 +1,25 @@
'use strict';
const ClientConstants = require('../constants/client');
const Packet = require('../packets/packet');
class SSLRequest {
constructor(flags, charset) {
this.clientFlags = flags | ClientConstants.SSL;
this.charset = charset;
}
toPacket() {
const length = 36;
const buffer = Buffer.allocUnsafe(length);
const packet = new Packet(0, buffer, 0, length);
buffer.fill(0);
packet.offset = 4;
packet.writeInt32(this.clientFlags);
packet.writeInt32(0); // max packet size. todo: move to config
packet.writeInt8(this.charset);
return packet;
}
}
module.exports = SSLRequest;
+47
View File
@@ -0,0 +1,47 @@
'use strict';
const Packet = require('../packets/packet');
class TextRow {
constructor(columns) {
this.columns = columns || [];
}
static fromPacket(packet) {
// packet.reset(); // set offset to starting point?
const columns = [];
while (packet.haveMoreData()) {
columns.push(packet.readLengthCodedString());
}
return new TextRow(columns);
}
static toPacket(columns, encoding) {
const sequenceId = 0; // TODO remove, this is calculated now in connecton
let length = 0;
columns.forEach((val) => {
if (val === null || typeof val === 'undefined') {
++length;
return;
}
length += Packet.lengthCodedStringLength(val.toString(10), encoding);
});
const buffer = Buffer.allocUnsafe(length + 4);
const packet = new Packet(sequenceId, buffer, 0, length + 4);
packet.offset = 4;
columns.forEach((val) => {
if (val === null) {
packet.writeNull();
return;
}
if (typeof val === 'undefined') {
packet.writeInt8(0);
return;
}
packet.writeLengthCodedString(val.toString(10), encoding);
});
return packet;
}
}
module.exports = TextRow;
+235
View File
@@ -0,0 +1,235 @@
'use strict';
const FieldFlags = require('../constants/field_flags.js');
const Charsets = require('../constants/charsets.js');
const Types = require('../constants/types.js');
const helpers = require('../helpers');
const genFunc = require('generate-function');
const parserCache = require('./parser_cache.js');
const typeNames = [];
for (const t in Types) {
typeNames[Types[t]] = t;
}
function readCodeFor(field, config, options, fieldNum) {
const supportBigNumbers = Boolean(
options.supportBigNumbers || config.supportBigNumbers
);
const bigNumberStrings = Boolean(
options.bigNumberStrings || config.bigNumberStrings
);
const timezone = options.timezone || config.timezone;
const dateStrings = options.dateStrings || config.dateStrings;
const unsigned = field.flags & FieldFlags.UNSIGNED;
switch (field.columnType) {
case Types.TINY:
return unsigned ? 'packet.readInt8();' : 'packet.readSInt8();';
case Types.SHORT:
return unsigned ? 'packet.readInt16();' : 'packet.readSInt16();';
case Types.LONG:
case Types.INT24: // in binary protocol int24 is encoded in 4 bytes int32
return unsigned ? 'packet.readInt32();' : 'packet.readSInt32();';
case Types.YEAR:
return 'packet.readInt16()';
case Types.FLOAT:
return 'packet.readFloat();';
case Types.DOUBLE:
return 'packet.readDouble();';
case Types.NULL:
return 'null;';
case Types.DATE:
case Types.DATETIME:
case Types.TIMESTAMP:
case Types.NEWDATE:
if (helpers.typeMatch(field.columnType, dateStrings, Types)) {
return `packet.readDateTimeString(${parseInt(field.decimals, 10)}, ${null}, ${field.columnType});`;
}
return `packet.readDateTime(${helpers.srcEscape(timezone)});`;
case Types.TIME:
return 'packet.readTimeString()';
case Types.DECIMAL:
case Types.NEWDECIMAL:
if (config.decimalNumbers) {
return 'packet.parseLengthCodedFloat();';
}
return 'packet.readLengthCodedString("ascii");';
case Types.GEOMETRY:
return 'packet.parseGeometryValue();';
case Types.VECTOR:
return 'packet.parseVector()';
case Types.JSON:
// Since for JSON columns mysql always returns charset 63 (BINARY),
// we have to handle it according to JSON specs and use "utf8",
// see https://github.com/sidorares/node-mysql2/issues/409
return config.jsonStrings
? 'packet.readLengthCodedString("utf8")'
: 'JSON.parse(packet.readLengthCodedString("utf8"));';
case Types.LONGLONG:
if (!supportBigNumbers) {
return unsigned
? 'packet.readInt64JSNumber();'
: 'packet.readSInt64JSNumber();';
}
if (bigNumberStrings) {
return unsigned
? 'packet.readInt64String();'
: 'packet.readSInt64String();';
}
return unsigned ? 'packet.readInt64();' : 'packet.readSInt64();';
default:
if (field.characterSet === Charsets.BINARY) {
return 'packet.readLengthCodedBuffer();';
}
return `packet.readLengthCodedString(fields[${fieldNum}].encoding)`;
}
}
function compile(fields, options, config) {
const parserFn = genFunc();
const nullBitmapLength = Math.floor((fields.length + 7 + 2) / 8);
function wrap(field, packet) {
return {
type: typeNames[field.columnType],
length: field.columnLength,
db: field.schema,
table: field.table,
name: field.name,
string: function (encoding = field.encoding) {
if (field.columnType === Types.JSON && encoding === field.encoding) {
// Since for JSON columns mysql always returns charset 63 (BINARY),
// we have to handle it according to JSON specs and use "utf8",
// see https://github.com/sidorares/node-mysql2/issues/1661
console.warn(
`typeCast: JSON column "${field.name}" is interpreted as BINARY by default, recommended to manually set utf8 encoding: \`field.string("utf8")\``
);
}
if (
[Types.DATETIME, Types.NEWDATE, Types.TIMESTAMP, Types.DATE].includes(
field.columnType
)
) {
return packet.readDateTimeString(
parseInt(field.decimals, 10),
' ',
field.columnType
);
}
if (field.columnType === Types.TINY) {
const unsigned = field.flags & FieldFlags.UNSIGNED;
return String(unsigned ? packet.readInt8() : packet.readSInt8());
}
if (field.columnType === Types.TIME) {
return packet.readTimeString();
}
return packet.readLengthCodedString(encoding);
},
buffer: function () {
return packet.readLengthCodedBuffer();
},
geometry: function () {
return packet.parseGeometryValue();
},
};
}
parserFn('(function(){');
parserFn('return class BinaryRow {');
parserFn('constructor() {');
parserFn('}');
parserFn('next(packet, fields, options) {');
if (options.rowsAsArray) {
parserFn(`const result = new Array(${fields.length});`);
} else {
parserFn('const result = {};');
}
// Global typeCast
if (
typeof config.typeCast === 'function' &&
typeof options.typeCast !== 'function'
) {
options.typeCast = config.typeCast;
}
parserFn('packet.readInt8();'); // status byte
for (let i = 0; i < nullBitmapLength; ++i) {
parserFn(`const nullBitmaskByte${i} = packet.readInt8();`);
}
let lvalue = '';
let currentFieldNullBit = 4;
let nullByteIndex = 0;
let fieldName = '';
let tableName = '';
for (let i = 0; i < fields.length; i++) {
fieldName = helpers.fieldEscape(fields[i].name);
// parserFn(`// ${fieldName}: ${typeNames[fields[i].columnType]}`);
if (typeof options.nestTables === 'string') {
lvalue = `result[${helpers.fieldEscape(fields[i].table + options.nestTables + fields[i].name)}]`;
} else if (options.nestTables === true) {
tableName = helpers.fieldEscape(fields[i].table);
parserFn(`if (!result[${tableName}]) result[${tableName}] = {};`);
lvalue = `result[${tableName}][${fieldName}]`;
} else if (options.rowsAsArray) {
lvalue = `result[${i.toString(10)}]`;
} else {
lvalue = `result[${fieldName}]`;
}
parserFn(`if (nullBitmaskByte${nullByteIndex} & ${currentFieldNullBit}) `);
parserFn(`${lvalue} = null;`);
parserFn('else {');
if (options.typeCast === false) {
parserFn(`${lvalue} = packet.readLengthCodedBuffer();`);
} else {
const fieldWrapperVar = `fieldWrapper${i}`;
parserFn(`const ${fieldWrapperVar} = wrap(fields[${i}], packet);`);
const readCode = readCodeFor(fields[i], config, options, i);
if (typeof options.typeCast === 'function') {
parserFn(
`${lvalue} = options.typeCast(${fieldWrapperVar}, function() { return ${readCode} });`
);
} else {
parserFn(`${lvalue} = ${readCode};`);
}
}
parserFn('}');
currentFieldNullBit *= 2;
if (currentFieldNullBit === 0x100) {
currentFieldNullBit = 1;
nullByteIndex++;
}
}
parserFn('return result;');
parserFn('}');
parserFn('};')('})()');
if (config.debug) {
helpers.printDebugWithCode(
'Compiled binary protocol row parser',
parserFn.toString()
);
}
return parserFn.toFunction({ wrap });
}
function getBinaryParser(fields, options, config) {
return parserCache.getParser('binary', fields, options, config, compile);
}
module.exports = getBinaryParser;
+68
View File
@@ -0,0 +1,68 @@
'use strict';
const { createLRU } = require('lru.min');
const parserCache = createLRU({
max: 15000,
});
function keyFromFields(type, fields, options, config) {
const res = [
type,
typeof options.nestTables,
options.nestTables,
Boolean(options.rowsAsArray),
Boolean(options.supportBigNumbers || config.supportBigNumbers),
Boolean(options.bigNumberStrings || config.bigNumberStrings),
typeof options.typeCast === 'boolean'
? options.typeCast
: typeof options.typeCast,
options.timezone || config.timezone,
Boolean(options.decimalNumbers),
options.dateStrings,
];
for (let i = 0; i < fields.length; ++i) {
const field = fields[i];
res.push([
field.name,
field.columnType,
field.length,
field.schema,
field.table,
field.flags,
field.characterSet,
]);
}
return JSON.stringify(res, null, 0);
}
function getParser(type, fields, options, config, compiler) {
const key = keyFromFields(type, fields, options, config);
let parser = parserCache.get(key);
if (parser) {
return parser;
}
parser = compiler(fields, options, config);
parserCache.set(key, parser);
return parser;
}
function setMaxCache(max) {
parserCache.resize(max);
}
function clearCache() {
parserCache.clear();
}
module.exports = {
getParser: getParser,
setMaxCache: setMaxCache,
clearCache: clearCache,
_keyFromFields: keyFromFields,
};
+213
View File
@@ -0,0 +1,213 @@
'use strict';
const FieldFlags = require('../constants/field_flags.js');
const Charsets = require('../constants/charsets.js');
const Types = require('../constants/types.js');
const helpers = require('../helpers');
const typeNames = [];
for (const t in Types) {
typeNames[Types[t]] = t;
}
function getBinaryParser(fields, _options, config) {
function readCode(field, config, options, fieldNum, packet) {
const supportBigNumbers = Boolean(
options.supportBigNumbers || config.supportBigNumbers
);
const bigNumberStrings = Boolean(
options.bigNumberStrings || config.bigNumberStrings
);
const timezone = options.timezone || config.timezone;
const dateStrings = options.dateStrings || config.dateStrings;
const unsigned = field.flags & FieldFlags.UNSIGNED;
switch (field.columnType) {
case Types.TINY:
return unsigned ? packet.readInt8() : packet.readSInt8();
case Types.SHORT:
return unsigned ? packet.readInt16() : packet.readSInt16();
case Types.LONG:
case Types.INT24: // in binary protocol int24 is encoded in 4 bytes int32
return unsigned ? packet.readInt32() : packet.readSInt32();
case Types.YEAR:
return packet.readInt16();
case Types.FLOAT:
return packet.readFloat();
case Types.DOUBLE:
return packet.readDouble();
case Types.NULL:
return null;
case Types.DATE:
case Types.DATETIME:
case Types.TIMESTAMP:
case Types.NEWDATE:
return helpers.typeMatch(field.columnType, dateStrings, Types)
? packet.readDateTimeString(
parseInt(field.decimals, 10),
null,
field.columnType
)
: packet.readDateTime(timezone);
case Types.TIME:
return packet.readTimeString();
case Types.DECIMAL:
case Types.NEWDECIMAL:
return config.decimalNumbers
? packet.parseLengthCodedFloat()
: packet.readLengthCodedString('ascii');
case Types.GEOMETRY:
return packet.parseGeometryValue();
case Types.VECTOR:
return packet.parseVector();
case Types.JSON:
// Since for JSON columns mysql always returns charset 63 (BINARY),
// we have to handle it according to JSON specs and use "utf8",
// see https://github.com/sidorares/node-mysql2/issues/409
return config.jsonStrings
? packet.readLengthCodedString('utf8')
: JSON.parse(packet.readLengthCodedString('utf8'));
case Types.LONGLONG:
if (!supportBigNumbers)
return unsigned
? packet.readInt64JSNumber()
: packet.readSInt64JSNumber();
return bigNumberStrings
? unsigned
? packet.readInt64String()
: packet.readSInt64String()
: unsigned
? packet.readInt64()
: packet.readSInt64();
default:
return field.characterSet === Charsets.BINARY
? packet.readLengthCodedBuffer()
: packet.readLengthCodedString(fields[fieldNum].encoding);
}
}
return class BinaryRow {
constructor() {}
next(packet, fields, options) {
packet.readInt8(); // status byte
const nullBitmapLength = Math.floor((fields.length + 7 + 2) / 8);
const nullBitmaskBytes = new Array(nullBitmapLength);
for (let i = 0; i < nullBitmapLength; i++) {
nullBitmaskBytes[i] = packet.readInt8();
}
const result = options.rowsAsArray ? new Array(fields.length) : {};
let currentFieldNullBit = 4;
let nullByteIndex = 0;
for (let i = 0; i < fields.length; i++) {
const field = fields[i];
const typeCast =
options.typeCast !== undefined ? options.typeCast : config.typeCast;
let value;
if (nullBitmaskBytes[nullByteIndex] & currentFieldNullBit) {
value = null;
} else if (options.typeCast === false) {
value = packet.readLengthCodedBuffer();
} else {
const next = () => readCode(field, config, options, i, packet);
value =
typeof typeCast === 'function'
? typeCast(
{
type: typeNames[field.columnType],
length: field.columnLength,
db: field.schema,
table: field.table,
name: field.name,
string: function (encoding = field.encoding) {
if (
field.columnType === Types.JSON &&
encoding === field.encoding
) {
// Since for JSON columns mysql always returns charset 63 (BINARY),
// we have to handle it according to JSON specs and use "utf8",
// see https://github.com/sidorares/node-mysql2/issues/1661
console.warn(
`typeCast: JSON column "${field.name}" is interpreted as BINARY by default, recommended to manually set utf8 encoding: \`field.string("utf8")\``
);
}
if (
[
Types.DATETIME,
Types.NEWDATE,
Types.TIMESTAMP,
Types.DATE,
].includes(field.columnType)
) {
return packet.readDateTimeString(
parseInt(field.decimals, 10),
' ',
field.columnType
);
}
if (field.columnType === Types.TINY) {
const unsigned = field.flags & FieldFlags.UNSIGNED;
return String(
unsigned ? packet.readInt8() : packet.readSInt8()
);
}
if (field.columnType === Types.TIME) {
return packet.readTimeString();
}
return packet.readLengthCodedString(encoding);
},
buffer: function () {
return packet.readLengthCodedBuffer();
},
geometry: function () {
return packet.parseGeometryValue();
},
},
next
)
: next();
}
if (options.rowsAsArray) {
result[i] = value;
} else if (typeof options.nestTables === 'string') {
const key = helpers.fieldEscape(
field.table + options.nestTables + field.name,
false
);
result[key] = value;
} else if (options.nestTables === true) {
const tableName = helpers.fieldEscape(field.table, false);
if (!result[tableName]) {
result[tableName] = {};
}
const fieldName = helpers.fieldEscape(field.name, false);
result[tableName][fieldName] = value;
} else {
const key = helpers.fieldEscape(field.name, false);
result[key] = value;
}
currentFieldNullBit *= 2;
if (currentFieldNullBit === 0x100) {
currentFieldNullBit = 1;
nullByteIndex++;
}
}
return result;
}
};
}
module.exports = getBinaryParser;
+152
View File
@@ -0,0 +1,152 @@
'use strict';
const Types = require('../constants/types.js');
const Charsets = require('../constants/charsets.js');
const helpers = require('../helpers');
const typeNames = [];
for (const t in Types) {
typeNames[Types[t]] = t;
}
function readField({ packet, type, charset, encoding, config, options }) {
const supportBigNumbers = Boolean(
options.supportBigNumbers || config.supportBigNumbers
);
const bigNumberStrings = Boolean(
options.bigNumberStrings || config.bigNumberStrings
);
const timezone = options.timezone || config.timezone;
const dateStrings = options.dateStrings || config.dateStrings;
switch (type) {
case Types.TINY:
case Types.SHORT:
case Types.LONG:
case Types.INT24:
case Types.YEAR:
return packet.parseLengthCodedIntNoBigCheck();
case Types.LONGLONG:
if (supportBigNumbers && bigNumberStrings) {
return packet.parseLengthCodedIntString();
}
return packet.parseLengthCodedInt(supportBigNumbers);
case Types.FLOAT:
case Types.DOUBLE:
return packet.parseLengthCodedFloat();
case Types.NULL:
case Types.DECIMAL:
case Types.NEWDECIMAL:
if (config.decimalNumbers) {
return packet.parseLengthCodedFloat();
}
return packet.readLengthCodedString('ascii');
case Types.DATE:
if (helpers.typeMatch(type, dateStrings, Types)) {
return packet.readLengthCodedString('ascii');
}
return packet.parseDate(timezone);
case Types.DATETIME:
case Types.TIMESTAMP:
if (helpers.typeMatch(type, dateStrings, Types)) {
return packet.readLengthCodedString('ascii');
}
return packet.parseDateTime(timezone);
case Types.TIME:
return packet.readLengthCodedString('ascii');
case Types.GEOMETRY:
return packet.parseGeometryValue();
case Types.VECTOR:
return packet.parseVector();
case Types.JSON:
// Since for JSON columns mysql always returns charset 63 (BINARY),
// we have to handle it according to JSON specs and use "utf8",
// see https://github.com/sidorares/node-mysql2/issues/409
return config.jsonStrings
? packet.readLengthCodedString('utf8')
: JSON.parse(packet.readLengthCodedString('utf8'));
default:
if (charset === Charsets.BINARY) {
return packet.readLengthCodedBuffer();
}
return packet.readLengthCodedString(encoding);
}
}
function createTypecastField(field, packet) {
return {
type: typeNames[field.columnType],
length: field.columnLength,
db: field.schema,
table: field.table,
name: field.name,
string: function (encoding = field.encoding) {
if (field.columnType === Types.JSON && encoding === field.encoding) {
// Since for JSON columns mysql always returns charset 63 (BINARY),
// we have to handle it according to JSON specs and use "utf8",
// see https://github.com/sidorares/node-mysql2/issues/1661
console.warn(
`typeCast: JSON column "${field.name}" is interpreted as BINARY by default, recommended to manually set utf8 encoding: \`field.string("utf8")\``
);
}
return packet.readLengthCodedString(encoding);
},
buffer: function () {
return packet.readLengthCodedBuffer();
},
geometry: function () {
return packet.parseGeometryValue();
},
};
}
function getTextParser(_fields, _options, config) {
return {
next(packet, fields, options) {
const result = options.rowsAsArray ? [] : {};
for (let i = 0; i < fields.length; i++) {
const field = fields[i];
const typeCast = options.typeCast ? options.typeCast : config.typeCast;
const next = () =>
readField({
packet,
type: field.columnType,
encoding: field.encoding,
charset: field.characterSet,
config,
options,
});
let value;
if (options.typeCast === false) {
value = packet.readLengthCodedBuffer();
} else if (typeof typeCast === 'function') {
value = typeCast(createTypecastField(field, packet), next);
} else {
value = next();
}
if (options.rowsAsArray) {
result.push(value);
} else if (typeof options.nestTables === 'string') {
result[
`${helpers.fieldEscape(field.table, false)}${options.nestTables}${helpers.fieldEscape(field.name, false)}`
] = value;
} else if (options.nestTables) {
const tableName = helpers.fieldEscape(field.table, false);
if (!result[tableName]) {
result[tableName] = {};
}
result[tableName][helpers.fieldEscape(field.name, false)] = value;
} else {
result[helpers.fieldEscape(field.name, false)] = value;
}
}
return result;
},
};
}
module.exports = getTextParser;
+50
View File
@@ -0,0 +1,50 @@
'use strict';
const Iconv = require('iconv-lite');
const { createLRU } = require('lru.min');
const decoderCache = createLRU({
max: 500,
});
exports.decode = function (buffer, encoding, start, end, options) {
if (Buffer.isEncoding(encoding)) {
return buffer.toString(encoding, start, end);
}
// Optimize for common case: encoding="short_string", options=undefined.
let decoder;
if (!options) {
decoder = decoderCache.get(encoding);
if (!decoder) {
decoder = Iconv.getDecoder(encoding);
decoderCache.set(encoding, decoder);
}
} else {
const decoderArgs = { encoding, options };
const decoderKey = JSON.stringify(decoderArgs);
decoder = decoderCache.get(decoderKey);
if (!decoder) {
decoder = Iconv.getDecoder(decoderArgs.encoding, decoderArgs.options);
decoderCache.set(decoderKey, decoder);
}
}
const res = decoder.write(buffer.slice(start, end));
const trail = decoder.end();
return trail ? res + trail : res;
};
exports.encode = function (string, encoding, options) {
if (Buffer.isEncoding(encoding)) {
return Buffer.from(string, encoding);
}
const encoder = Iconv.getEncoder(encoding, options || {});
const res = encoder.write(string);
const trail = encoder.end();
return trail && trail.length > 0 ? Buffer.concat([res, trail]) : res;
};
+214
View File
@@ -0,0 +1,214 @@
'use strict';
const Types = require('../constants/types.js');
const Charsets = require('../constants/charsets.js');
const helpers = require('../helpers');
const genFunc = require('generate-function');
const parserCache = require('./parser_cache.js');
const typeNames = [];
for (const t in Types) {
typeNames[Types[t]] = t;
}
function readCodeFor(type, charset, encodingExpr, config, options) {
const supportBigNumbers = Boolean(
options.supportBigNumbers || config.supportBigNumbers
);
const bigNumberStrings = Boolean(
options.bigNumberStrings || config.bigNumberStrings
);
const timezone = options.timezone || config.timezone;
const dateStrings = options.dateStrings || config.dateStrings;
switch (type) {
case Types.TINY:
case Types.SHORT:
case Types.LONG:
case Types.INT24:
case Types.YEAR:
return 'packet.parseLengthCodedIntNoBigCheck()';
case Types.LONGLONG:
if (supportBigNumbers && bigNumberStrings) {
return 'packet.parseLengthCodedIntString()';
}
return `packet.parseLengthCodedInt(${supportBigNumbers})`;
case Types.FLOAT:
case Types.DOUBLE:
return 'packet.parseLengthCodedFloat()';
case Types.NULL:
return 'packet.readLengthCodedNumber()';
case Types.DECIMAL:
case Types.NEWDECIMAL:
if (config.decimalNumbers) {
return 'packet.parseLengthCodedFloat()';
}
return 'packet.readLengthCodedString("ascii")';
case Types.DATE:
if (helpers.typeMatch(type, dateStrings, Types)) {
return 'packet.readLengthCodedString("ascii")';
}
return `packet.parseDate(${helpers.srcEscape(timezone)})`;
case Types.DATETIME:
case Types.TIMESTAMP:
if (helpers.typeMatch(type, dateStrings, Types)) {
return 'packet.readLengthCodedString("ascii")';
}
return `packet.parseDateTime(${helpers.srcEscape(timezone)})`;
case Types.TIME:
return 'packet.readLengthCodedString("ascii")';
case Types.GEOMETRY:
return 'packet.parseGeometryValue()';
case Types.VECTOR:
return 'packet.parseVector()';
case Types.JSON:
// Since for JSON columns mysql always returns charset 63 (BINARY),
// we have to handle it according to JSON specs and use "utf8",
// see https://github.com/sidorares/node-mysql2/issues/409
return config.jsonStrings
? 'packet.readLengthCodedString("utf8")'
: 'JSON.parse(packet.readLengthCodedString("utf8"))';
default:
if (charset === Charsets.BINARY) {
return 'packet.readLengthCodedBuffer()';
}
return `packet.readLengthCodedString(${encodingExpr})`;
}
}
function compile(fields, options, config) {
// use global typeCast if current query doesn't specify one
if (
typeof config.typeCast === 'function' &&
typeof options.typeCast !== 'function'
) {
options.typeCast = config.typeCast;
}
function wrap(field, _this) {
return {
type: typeNames[field.columnType],
length: field.columnLength,
db: field.schema,
table: field.table,
name: field.name,
string: function (encoding = field.encoding) {
if (field.columnType === Types.JSON && encoding === field.encoding) {
// Since for JSON columns mysql always returns charset 63 (BINARY),
// we have to handle it according to JSON specs and use "utf8",
// see https://github.com/sidorares/node-mysql2/issues/1661
console.warn(
`typeCast: JSON column "${field.name}" is interpreted as BINARY by default, recommended to manually set utf8 encoding: \`field.string("utf8")\``
);
}
return _this.packet.readLengthCodedString(encoding);
},
buffer: function () {
return _this.packet.readLengthCodedBuffer();
},
geometry: function () {
return _this.packet.parseGeometryValue();
},
};
}
const parserFn = genFunc();
parserFn('(function () {')('return class TextRow {');
// constructor method
parserFn('constructor(fields) {');
// node-mysql typeCast compatibility wrapper
// see https://github.com/mysqljs/mysql/blob/96fdd0566b654436624e2375c7b6604b1f50f825/lib/protocol/packets/Field.js
if (typeof options.typeCast === 'function') {
parserFn('const _this = this;');
parserFn('for(let i=0; i<fields.length; ++i) {');
parserFn('this[`wrap${i}`] = wrap(fields[i], _this);');
parserFn('}');
}
parserFn('}');
// next method
parserFn('next(packet, fields, options) {');
parserFn('this.packet = packet;');
if (options.rowsAsArray) {
parserFn(`const result = new Array(${fields.length});`);
} else {
parserFn('const result = {};');
}
const resultTables = {};
let resultTablesArray = [];
if (options.nestTables === true) {
for (let i = 0; i < fields.length; i++) {
resultTables[fields[i].table] = 1;
}
resultTablesArray = Object.keys(resultTables);
for (let i = 0; i < resultTablesArray.length; i++) {
parserFn(`result[${helpers.fieldEscape(resultTablesArray[i])}] = {};`);
}
}
let lvalue = '';
let fieldName = '';
let tableName = '';
for (let i = 0; i < fields.length; i++) {
fieldName = helpers.fieldEscape(fields[i].name);
// parserFn(`// ${fieldName}: ${typeNames[fields[i].columnType]}`);
if (typeof options.nestTables === 'string') {
lvalue = `result[${helpers.fieldEscape(fields[i].table + options.nestTables + fields[i].name)}]`;
} else if (options.nestTables === true) {
tableName = helpers.fieldEscape(fields[i].table);
parserFn(`if (!result[${tableName}]) result[${tableName}] = {};`);
lvalue = `result[${tableName}][${fieldName}]`;
} else if (options.rowsAsArray) {
lvalue = `result[${i.toString(10)}]`;
} else {
lvalue = `result[${fieldName}]`;
}
if (options.typeCast === false) {
parserFn(`${lvalue} = packet.readLengthCodedBuffer();`);
} else {
const encodingExpr = `fields[${i}].encoding`;
const readCode = readCodeFor(
fields[i].columnType,
fields[i].characterSet,
encodingExpr,
config,
options
);
if (typeof options.typeCast === 'function') {
parserFn(
`${lvalue} = options.typeCast(this.wrap${i}, function() { return ${readCode} });`
);
} else {
parserFn(`${lvalue} = ${readCode};`);
}
}
}
parserFn('return result;');
parserFn('}');
parserFn('};')('})()');
if (config.debug) {
helpers.printDebugWithCode(
'Compiled text protocol row parser',
parserFn.toString()
);
}
if (typeof options.typeCast === 'function') {
return parserFn.toFunction({ wrap });
}
return parserFn.toFunction();
}
function getTextParser(fields, options, config) {
return parserCache.getParser('text', fields, options, config, compile);
}
module.exports = getTextParser;
+12
View File
@@ -0,0 +1,12 @@
'use strict';
const BasePool = require('./base/pool.js');
class Pool extends BasePool {
promise(promiseImpl) {
const PromisePool = require('./promise/pool.js');
return new PromisePool(this, promiseImpl);
}
}
module.exports = Pool;
+375
View File
@@ -0,0 +1,375 @@
'use strict';
const process = require('process');
const Pool = require('./pool.js');
const PoolConfig = require('./pool_config.js');
const Connection = require('./connection.js');
const EventEmitter = require('events').EventEmitter;
/**
* Selector
*/
const makeSelector = {
RR() {
let index = 0;
return (clusterIds) => clusterIds[index++ % clusterIds.length];
},
RANDOM() {
return (clusterIds) =>
clusterIds[Math.floor(Math.random() * clusterIds.length)];
},
ORDER() {
return (clusterIds) => clusterIds[0];
},
};
const getMonotonicMilliseconds = function () {
let ms;
if (typeof process.hrtime === 'function') {
ms = process.hrtime();
ms = ms[0] * 1e3 + ms[1] * 1e-6;
} else {
ms = process.uptime() * 1000;
}
return Math.floor(ms);
};
const patternRegExp = function (pattern) {
if (pattern instanceof RegExp) {
return pattern;
}
const source = pattern
.replace(/([.+?^=!:${}()|[\]/\\])/g, '\\$1')
.replace(/\*/g, '.*');
return new RegExp(`^${source}$`);
};
class PoolNamespace {
constructor(cluster, pattern, selector) {
this._cluster = cluster;
this._pattern = pattern;
this._selector = makeSelector[selector]();
}
getConnection(cb) {
const clusterNode = this._getClusterNode();
if (clusterNode === null) {
let err = new Error('Pool does Not exist.');
err.code = 'POOL_NOEXIST';
if (this._cluster._findNodeIds(this._pattern, true).length !== 0) {
err = new Error('Pool does Not have online node.');
err.code = 'POOL_NONEONLINE';
}
return cb(err);
}
return this._cluster._getConnection(clusterNode, (err, connection) => {
if (err) {
if (
this._cluster._canRetry &&
this._cluster._findNodeIds(this._pattern).length !== 0
) {
this._cluster.emit('warn', err);
return this.getConnection(cb);
}
return cb(err);
}
return cb(null, connection);
});
}
/**
* pool cluster query
* @param {*} sql
* @param {*} values
* @param {*} cb
* @returns query
*/
query(sql, values, cb) {
const query = Connection.createQuery(sql, values, cb, {});
this.getConnection((err, conn) => {
if (err) {
if (typeof query.onResult === 'function') {
query.onResult(err);
} else {
query.emit('error', err);
}
return;
}
try {
conn.query(query).once('end', () => {
conn.release();
});
} catch (e) {
conn.release();
throw e;
}
});
return query;
}
/**
* pool cluster execute
* @param {*} sql
* @param {*} values
* @param {*} cb
*/
execute(sql, values, cb) {
if (typeof values === 'function') {
cb = values;
values = [];
}
this.getConnection((err, conn) => {
if (err) {
return cb(err);
}
try {
conn.execute(sql, values, cb).once('end', () => {
conn.release();
});
} catch (e) {
conn.release();
throw e;
}
});
}
_getClusterNode() {
const foundNodeIds = this._cluster._findNodeIds(this._pattern);
if (foundNodeIds.length === 0) {
return null;
}
const nodeId =
foundNodeIds.length === 1
? foundNodeIds[0]
: this._selector(foundNodeIds);
return this._cluster._getNode(nodeId);
}
}
class PoolCluster extends EventEmitter {
constructor(config) {
super();
config = config || {};
this._canRetry =
typeof config.canRetry === 'undefined' ? true : config.canRetry;
this._removeNodeErrorCount = config.removeNodeErrorCount || 5;
this._restoreNodeTimeout = config.restoreNodeTimeout || 0;
this._defaultSelector = config.defaultSelector || 'RR';
this._closed = false;
this._lastId = 0;
this._nodes = {};
this._serviceableNodeIds = [];
this._namespaces = {};
this._findCaches = {};
}
of(pattern, selector) {
pattern = pattern || '*';
selector = selector || this._defaultSelector;
selector = selector.toUpperCase();
if (!makeSelector[selector] === 'undefined') {
selector = this._defaultSelector;
}
const key = pattern + selector;
if (typeof this._namespaces[key] === 'undefined') {
this._namespaces[key] = new PoolNamespace(this, pattern, selector);
}
return this._namespaces[key];
}
add(id, config) {
if (typeof id === 'object') {
config = id;
id = `CLUSTER::${++this._lastId}`;
}
if (typeof this._nodes[id] === 'undefined') {
this._nodes[id] = {
id: id,
errorCount: 0,
pool: new Pool({ config: new PoolConfig(config) }),
_offlineUntil: 0,
};
this._serviceableNodeIds.push(id);
this._clearFindCaches();
}
}
remove(pattern) {
const foundNodeIds = this._findNodeIds(pattern, true);
for (let i = 0; i < foundNodeIds.length; i++) {
const node = this._getNode(foundNodeIds[i]);
if (node) {
this._removeNode(node);
}
}
}
getConnection(pattern, selector, cb) {
let namespace;
if (typeof pattern === 'function') {
cb = pattern;
namespace = this.of();
} else {
if (typeof selector === 'function') {
cb = selector;
selector = this._defaultSelector;
}
namespace = this.of(pattern, selector);
}
namespace.getConnection(cb);
}
[Symbol.dispose]() {
if (!this._closed) {
this.end();
}
}
end(callback) {
const cb =
callback !== undefined
? callback
: (err) => {
if (err) {
throw err;
}
};
if (this._closed) {
process.nextTick(cb);
return;
}
this._closed = true;
let calledBack = false;
let waitingClose = 0;
const onEnd = (err) => {
if (!calledBack && (err || --waitingClose <= 0)) {
calledBack = true;
return cb(err);
}
};
for (const id in this._nodes) {
waitingClose++;
this._nodes[id].pool.end(onEnd);
}
if (waitingClose === 0) {
process.nextTick(onEnd);
}
}
_findNodeIds(pattern, includeOffline) {
let currentTime = 0;
let foundNodeIds = this._findCaches[pattern];
if (foundNodeIds === undefined) {
const expression = patternRegExp(pattern);
foundNodeIds = this._serviceableNodeIds.filter((id) =>
id.match(expression)
);
}
this._findCaches[pattern] = foundNodeIds;
if (includeOffline) {
return foundNodeIds;
}
return foundNodeIds.filter((nodeId) => {
const node = this._getNode(nodeId);
if (!node._offlineUntil) {
return true;
}
if (!currentTime) {
currentTime = getMonotonicMilliseconds();
}
return node._offlineUntil <= currentTime;
});
}
_getNode(id) {
return this._nodes[id] || null;
}
_increaseErrorCount(node) {
const errorCount = ++node.errorCount;
if (this._removeNodeErrorCount > errorCount) {
return;
}
if (this._restoreNodeTimeout > 0) {
node._offlineUntil =
getMonotonicMilliseconds() + this._restoreNodeTimeout;
this.emit('offline', node.id);
return;
}
this._removeNode(node);
this.emit('remove', node.id);
}
_decreaseErrorCount(node) {
let errorCount = node.errorCount;
if (errorCount > this._removeNodeErrorCount) {
errorCount = this._removeNodeErrorCount;
}
if (errorCount < 1) {
errorCount = 1;
}
node.errorCount = errorCount - 1;
if (node._offlineUntil) {
node._offlineUntil = 0;
this.emit('online', node.id);
}
}
_getConnection(node, cb) {
node.pool.getConnection((err, connection) => {
if (err) {
this._increaseErrorCount(node);
return cb(err);
}
this._decreaseErrorCount(node);
connection._clusterId = node.id;
return cb(null, connection);
});
}
_removeNode(node) {
const index = this._serviceableNodeIds.indexOf(node.id);
if (index !== -1) {
this._serviceableNodeIds.splice(index, 1);
delete this._nodes[node.id];
this._clearFindCaches();
node.pool.end();
}
}
_clearFindCaches() {
this._findCaches = {};
}
}
module.exports = PoolCluster;
+34
View File
@@ -0,0 +1,34 @@
'use strict';
const ConnectionConfig = require('./connection_config.js');
class PoolConfig {
constructor(options) {
if (typeof options === 'string') {
options = ConnectionConfig.parseUrl(options);
}
this.connectionConfig = new ConnectionConfig(options);
this.waitForConnections =
options.waitForConnections === undefined
? true
: Boolean(options.waitForConnections);
this.connectionLimit = isNaN(options.connectionLimit)
? 10
: Number(options.connectionLimit);
this.maxIdle = isNaN(options.maxIdle)
? this.connectionLimit
: Number(options.maxIdle);
this.idleTimeout = isNaN(options.idleTimeout)
? 60000
: Number(options.idleTimeout);
this.queueLimit = isNaN(options.queueLimit)
? 0
: Number(options.queueLimit);
this.resetOnRelease =
options.resetOnRelease === undefined
? false
: Boolean(options.resetOnRelease);
}
}
module.exports = PoolConfig;
+81
View File
@@ -0,0 +1,81 @@
'use strict';
const Connection = require('./connection.js');
class PoolConnection extends Connection {
constructor(pool, options) {
super(options);
this._pool = pool;
this._released = false;
this.lastActiveTime = Date.now();
this.once('end', () => {
this._removeFromPool();
});
this.once('error', () => {
this._removeFromPool();
});
}
release() {
if (this._released) {
return;
}
if (!this._pool || this._pool._closed) {
return;
}
this._released = true;
this.lastActiveTime = Date.now();
this._pool.releaseConnection(this);
}
[Symbol.dispose]() {
this.release();
}
end(callback) {
if (this.config.gracefulEnd) {
this._removeFromPool();
super.end(callback);
return;
}
const err = new Error(
'Calling conn.end() to release a pooled connection is ' +
'deprecated. In next version calling conn.end() will be ' +
'restored to default conn.end() behavior. Use ' +
'conn.release() instead.'
);
this.emit('warn', err);
console.warn(err.message);
this.release();
if (typeof callback === 'function') {
callback();
}
}
destroy() {
this._removeFromPool();
super.destroy();
}
_removeFromPool() {
if (!this._pool || this._pool._closed) {
return;
}
const pool = this._pool;
this._pool = null;
pool._removeConnection(this);
}
promise(promiseImpl) {
const PromisePoolConnection = require('./promise/pool_connection.js');
return new PromisePoolConnection(this, promiseImpl);
}
}
PoolConnection.statementKey = Connection.statementKey;
module.exports = PoolConnection;
// TODO: Remove this when we are removing PoolConnection#end
PoolConnection.prototype._realEnd = Connection.prototype.end;
+25
View File
@@ -0,0 +1,25 @@
'use strict';
/** @param {Function} constructorOpt Passed to Error.captureStackTrace (omit this frame from stacks). */
function captureStackHolder(constructorOpt) {
const holder = {};
Error.captureStackTrace(holder, constructorOpt);
return holder;
}
/**
* Replace `err.stack` frames with the capture from `holder`, keeping the
* callback error instance and its MySQL fields.
*
* @param {Error} err
* @param {{ stack?: string }} holder
*/
function applyCapturedStack(err, holder) {
const stack = holder && holder.stack;
if (typeof stack !== 'string' || !stack) return;
const lines = stack.split('\n');
lines[0] = `${err.name}: ${err.message}`;
err.stack = lines.join('\n');
}
module.exports = { captureStackHolder, applyCapturedStack };
+237
View File
@@ -0,0 +1,237 @@
'use strict';
const EventEmitter = require('events').EventEmitter;
const PromisePreparedStatementInfo = require('./prepared_statement_info.js');
const {
captureStackHolder,
applyCapturedStack,
} = require('./capture_local_err.js');
const makeDoneCb = require('./make_done_cb.js');
const inheritEvents = require('./inherit_events.js');
const BaseConnection = require('../base/connection.js');
class PromiseConnection extends EventEmitter {
constructor(connection, promiseImpl) {
super();
this.connection = connection;
this.Promise = promiseImpl || Promise;
inheritEvents(connection, this, [
'error',
'drain',
'connect',
'end',
'enqueue',
]);
}
release() {
this.connection.release();
}
query(query, params) {
const c = this.connection;
const stackHolder = captureStackHolder(PromiseConnection.prototype.query);
if (typeof params === 'function') {
throw new Error(
'Callback function is not available with promise clients.'
);
}
return new this.Promise((resolve, reject) => {
const done = makeDoneCb(resolve, reject, stackHolder);
if (params !== undefined) {
c.query(query, params, done);
} else {
c.query(query, done);
}
});
}
execute(query, params) {
const c = this.connection;
const stackHolder = captureStackHolder(PromiseConnection.prototype.execute);
if (typeof params === 'function') {
throw new Error(
'Callback function is not available with promise clients.'
);
}
return new this.Promise((resolve, reject) => {
const done = makeDoneCb(resolve, reject, stackHolder);
if (params !== undefined) {
c.execute(query, params, done);
} else {
c.execute(query, done);
}
});
}
end() {
return new this.Promise((resolve) => {
this.connection.end(resolve);
});
}
async [Symbol.asyncDispose]() {
if (!this.connection._closing) {
await this.end();
}
}
beginTransaction() {
const c = this.connection;
const stackHolder = captureStackHolder(
PromiseConnection.prototype.beginTransaction
);
return new this.Promise((resolve, reject) => {
const done = makeDoneCb(resolve, reject, stackHolder);
c.beginTransaction(done);
});
}
commit() {
const c = this.connection;
const stackHolder = captureStackHolder(PromiseConnection.prototype.commit);
return new this.Promise((resolve, reject) => {
const done = makeDoneCb(resolve, reject, stackHolder);
c.commit(done);
});
}
rollback() {
const c = this.connection;
const stackHolder = captureStackHolder(
PromiseConnection.prototype.rollback
);
return new this.Promise((resolve, reject) => {
const done = makeDoneCb(resolve, reject, stackHolder);
c.rollback(done);
});
}
ping() {
const c = this.connection;
const stackHolder = captureStackHolder(PromiseConnection.prototype.ping);
return new this.Promise((resolve, reject) => {
c.ping((err) => {
if (err) {
applyCapturedStack(err, stackHolder);
reject(err);
} else {
resolve(true);
}
});
});
}
reset() {
const c = this.connection;
const stackHolder = captureStackHolder(PromiseConnection.prototype.reset);
return new this.Promise((resolve, reject) => {
c.reset((err) => {
if (err) {
applyCapturedStack(err, stackHolder);
reject(err);
} else {
resolve();
}
});
});
}
connect() {
const c = this.connection;
const stackHolder = captureStackHolder(PromiseConnection.prototype.connect);
return new this.Promise((resolve, reject) => {
c.connect((err, param) => {
if (err) {
applyCapturedStack(err, stackHolder);
reject(err);
} else {
resolve(param);
}
});
});
}
prepare(options) {
const c = this.connection;
const promiseImpl = this.Promise;
const stackHolder = captureStackHolder(PromiseConnection.prototype.prepare);
return new this.Promise((resolve, reject) => {
c.prepare(options, (err, statement) => {
if (err) {
applyCapturedStack(err, stackHolder);
reject(err);
} else {
const wrappedStatement = new PromisePreparedStatementInfo(
statement,
promiseImpl
);
resolve(wrappedStatement);
}
});
});
}
changeUser(options) {
const c = this.connection;
const stackHolder = captureStackHolder(
PromiseConnection.prototype.changeUser
);
return new this.Promise((resolve, reject) => {
c.changeUser(options, (err) => {
if (err) {
applyCapturedStack(err, stackHolder);
reject(err);
} else {
resolve();
}
});
});
}
get config() {
return this.connection.config;
}
get threadId() {
return this.connection.threadId;
}
}
// patching PromiseConnection
// create facade functions for prototype functions on "Connection" that are not yet
// implemented with PromiseConnection
// proxy synchronous functions only
(function (functionsToWrap) {
for (let i = 0; functionsToWrap && i < functionsToWrap.length; i++) {
const func = functionsToWrap[i];
if (
typeof BaseConnection.prototype[func] === 'function' &&
PromiseConnection.prototype[func] === undefined
) {
PromiseConnection.prototype[func] = (function factory(funcName) {
return function () {
return BaseConnection.prototype[funcName].apply(
this.connection,
arguments
);
};
})(func);
}
}
})([
// synchronous functions
'close',
'createBinlogStream',
'destroy',
'escape',
'escapeId',
'format',
'pause',
'pipe',
'resume',
'unprepare',
]);
module.exports = PromiseConnection;
+27
View File
@@ -0,0 +1,27 @@
'use strict';
function inheritEvents(source, target, events) {
const listeners = {};
target
.on('newListener', (eventName) => {
if (events.indexOf(eventName) >= 0 && !target.listenerCount(eventName)) {
source.on(
eventName,
(listeners[eventName] = function () {
const args = [].slice.call(arguments);
args.unshift(eventName);
target.emit.apply(target, args);
})
);
}
})
.on('removeListener', (eventName) => {
if (events.indexOf(eventName) >= 0 && !target.listenerCount(eventName)) {
source.removeListener(eventName, listeners[eventName]);
delete listeners[eventName];
}
});
}
module.exports = inheritEvents;
+16
View File
@@ -0,0 +1,16 @@
'use strict';
const { applyCapturedStack } = require('./capture_local_err.js');
function makeDoneCb(resolve, reject, stackHolder) {
return function (err, rows, fields) {
if (err) {
applyCapturedStack(err, stackHolder);
reject(err);
} else {
resolve([rows, fields]);
}
};
}
module.exports = makeDoneCb;
+118
View File
@@ -0,0 +1,118 @@
'use strict';
const EventEmitter = require('events').EventEmitter;
const {
captureStackHolder,
applyCapturedStack,
} = require('./capture_local_err.js');
const makeDoneCb = require('./make_done_cb.js');
const PromisePoolConnection = require('./pool_connection.js');
const inheritEvents = require('./inherit_events.js');
const BasePool = require('../base/pool.js');
class PromisePool extends EventEmitter {
constructor(pool, thePromise) {
super();
this.pool = pool;
this.Promise = thePromise || Promise;
inheritEvents(pool, this, ['acquire', 'connection', 'enqueue', 'release']);
}
getConnection() {
const corePool = this.pool;
return new this.Promise((resolve, reject) => {
corePool.getConnection((err, coreConnection) => {
if (err) {
reject(err);
} else {
resolve(new PromisePoolConnection(coreConnection, this.Promise));
}
});
});
}
releaseConnection(connection) {
if (connection instanceof PromisePoolConnection) connection.release();
}
query(sql, args) {
const corePool = this.pool;
const stackHolder = captureStackHolder(PromisePool.prototype.query);
if (typeof args === 'function') {
throw new Error(
'Callback function is not available with promise clients.'
);
}
return new this.Promise((resolve, reject) => {
const done = makeDoneCb(resolve, reject, stackHolder);
if (args !== undefined) {
corePool.query(sql, args, done);
} else {
corePool.query(sql, done);
}
});
}
execute(sql, args) {
const corePool = this.pool;
const stackHolder = captureStackHolder(PromisePool.prototype.execute);
if (typeof args === 'function') {
throw new Error(
'Callback function is not available with promise clients.'
);
}
return new this.Promise((resolve, reject) => {
const done = makeDoneCb(resolve, reject, stackHolder);
if (args) {
corePool.execute(sql, args, done);
} else {
corePool.execute(sql, done);
}
});
}
end() {
const corePool = this.pool;
const stackHolder = captureStackHolder(PromisePool.prototype.end);
return new this.Promise((resolve, reject) => {
corePool.end((err) => {
if (err) {
applyCapturedStack(err, stackHolder);
reject(err);
} else {
resolve();
}
});
});
}
async [Symbol.asyncDispose]() {
if (!this.pool._closed) {
await this.end();
}
}
}
(function (functionsToWrap) {
for (let i = 0; functionsToWrap && i < functionsToWrap.length; i++) {
const func = functionsToWrap[i];
if (
typeof BasePool.prototype[func] === 'function' &&
PromisePool.prototype[func] === undefined
) {
PromisePool.prototype[func] = (function factory(funcName) {
return function () {
return BasePool.prototype[funcName].apply(this.pool, arguments);
};
})(func);
}
}
})([
// synchronous functions
'escape',
'escapeId',
'format',
]);
module.exports = PromisePool;
+59
View File
@@ -0,0 +1,59 @@
'use strict';
const { captureStackHolder } = require('./capture_local_err.js');
const PromisePoolConnection = require('./pool_connection');
const makeDoneCb = require('./make_done_cb');
class PromisePoolNamespace {
constructor(poolNamespace, thePromise) {
this.poolNamespace = poolNamespace;
this.Promise = thePromise || Promise;
}
getConnection() {
const corePoolNamespace = this.poolNamespace;
return new this.Promise((resolve, reject) => {
corePoolNamespace.getConnection((err, coreConnection) => {
if (err) {
reject(err);
} else {
resolve(new PromisePoolConnection(coreConnection, this.Promise));
}
});
});
}
query(sql, values) {
const corePoolNamespace = this.poolNamespace;
const stackHolder = captureStackHolder(
PromisePoolNamespace.prototype.query
);
if (typeof values === 'function') {
throw new Error(
'Callback function is not available with promise clients.'
);
}
return new this.Promise((resolve, reject) => {
const done = makeDoneCb(resolve, reject, stackHolder);
corePoolNamespace.query(sql, values, done);
});
}
execute(sql, values) {
const corePoolNamespace = this.poolNamespace;
const stackHolder = captureStackHolder(
PromisePoolNamespace.prototype.execute
);
if (typeof values === 'function') {
throw new Error(
'Callback function is not available with promise clients.'
);
}
return new this.Promise((resolve, reject) => {
const done = makeDoneCb(resolve, reject, stackHolder);
corePoolNamespace.execute(sql, values, done);
});
}
}
module.exports = PromisePoolNamespace;
+19
View File
@@ -0,0 +1,19 @@
'use strict';
const PromiseConnection = require('./connection.js');
class PromisePoolConnection extends PromiseConnection {
constructor(connection, promiseImpl) {
super(connection, promiseImpl);
}
destroy() {
return this.connection.destroy();
}
async [Symbol.asyncDispose]() {
this.release();
}
}
module.exports = PromisePoolConnection;
+35
View File
@@ -0,0 +1,35 @@
'use strict';
const { captureStackHolder } = require('./capture_local_err.js');
const makeDoneCb = require('./make_done_cb.js');
class PromisePreparedStatementInfo {
constructor(statement, promiseImpl) {
this.statement = statement;
this.Promise = promiseImpl;
}
execute(parameters) {
const s = this.statement;
const stackHolder = captureStackHolder(
PromisePreparedStatementInfo.prototype.execute
);
return new this.Promise((resolve, reject) => {
const done = makeDoneCb(resolve, reject, stackHolder);
if (parameters) {
s.execute(parameters, done);
} else {
s.execute(done);
}
});
}
close() {
return new this.Promise((resolve) => {
this.statement.close();
resolve();
});
}
}
module.exports = PromisePreparedStatementInfo;
+38
View File
@@ -0,0 +1,38 @@
'use strict';
const Readable = require('stream').Readable;
// copy-paste from https://github.com/mysqljs/mysql/blob/master/lib/protocol/sequences/Query.js
module.exports = function (command, connectionStream) {
command.stream = function (options) {
let stream;
options = options || {};
options.objectMode = true;
((stream = new Readable(options)),
(stream._read = function () {
connectionStream.resume();
}));
this.on('result', (row, i) => {
if (!stream.push(row)) {
connectionStream.pause();
}
stream.emit('result', row, i); // replicate old emitter
});
this.on('error', (err) => {
stream.emit('error', err); // Pass on any errors
});
this.on('end', () => {
stream.push(null); // pushing null, indicating EOF
});
this.on('fields', (fields, i) => {
stream.emit('fields', fields, i); // replicate old emitter
});
return stream;
};
};
+37
View File
@@ -0,0 +1,37 @@
'use strict';
const net = require('net');
const EventEmitter = require('events').EventEmitter;
const Connection = require('./connection');
const ConnectionConfig = require('./connection_config');
// TODO: inherit Server from net.Server
class Server extends EventEmitter {
constructor() {
super();
this.connections = [];
this._server = net.createServer(this._handleConnection.bind(this));
}
_handleConnection(socket) {
const connectionConfig = new ConnectionConfig({
stream: socket,
isServer: true,
});
const connection = new Connection({ config: connectionConfig });
this.emit('connection', connection);
}
listen(port) {
this._port = port;
this._server.listen.apply(this._server, arguments);
return this;
}
close(cb) {
this._server.close(cb);
}
}
module.exports = Server;
+81
View File
@@ -0,0 +1,81 @@
'use strict';
const process = require('process');
// Safe load: use getBuiltinModule if available, fallback to require, catch if unavailable
const dc = (() => {
try {
return 'getBuiltinModule' in process
? process.getBuiltinModule('node:diagnostics_channel')
: require('node:diagnostics_channel');
} catch {
return undefined;
}
})();
const hasTracingChannel = typeof dc?.tracingChannel === 'function';
const queryChannel = hasTracingChannel
? dc.tracingChannel('mysql2:query')
: undefined;
const executeChannel = hasTracingChannel
? dc.tracingChannel('mysql2:execute')
: undefined;
const connectChannel = hasTracingChannel
? dc.tracingChannel('mysql2:connect')
: undefined;
const poolConnectChannel = hasTracingChannel
? dc.tracingChannel('mysql2:pool:connect')
: undefined;
function getServerContext(config) {
if (config.socketPath) {
return { serverAddress: config.socketPath, serverPort: undefined };
}
return {
serverAddress: config.host || 'localhost',
serverPort: config.port || 3306,
};
}
// Node 20+: TracingChannel has an aggregated hasSubscribers getter.
// Node 18.x: that getter is missing (undefined), fall back to start sub-channel.
function shouldTrace(channel) {
if (channel === undefined || channel === null) {
return false;
}
return channel.hasSubscribers ?? channel.start?.hasSubscribers ?? false;
}
// Generic traceCallback wrapper — calls fn synchronously, wraps the callback
// at args[position] to emit asyncStart/asyncEnd/error. No promises involved.
function traceCallback(channel, fn, position, context, thisArg, ...args) {
if (shouldTrace(channel)) {
return channel.traceCallback(fn, position, context(), thisArg, ...args);
}
return fn.apply(thisArg, args);
}
// tracePromise for operations that are inherently async (connection handshake)
function tracePromise(channel, fn, contextFactory) {
if (shouldTrace(channel)) {
return channel.tracePromise(fn, contextFactory());
}
return fn();
}
module.exports = {
dc,
hasTracingChannel,
shouldTrace,
queryChannel,
executeChannel,
connectChannel,
poolConnectChannel,
getServerContext,
traceCallback,
tracePromise,
};
+21
View File
@@ -0,0 +1,21 @@
Copyright (c) 2011 Alexander Shtuchkin
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+138
View File
@@ -0,0 +1,138 @@
## iconv-lite: Pure JS character encoding conversion
[![NPM Version][npm-version-image]][npm-url]
[![NPM Downloads][npm-downloads-image]][npm-downloads-url]
[![License][license-image]][license-url]
[![NPM Install Size][npm-install-size-image]][npm-install-size-url]
* No need for native code compilation. Quick to install, works on Windows, Web, and in sandboxed environments.
* Used in popular projects like [Express.js (body_parser)](https://github.com/expressjs/body-parser),
[Grunt](http://gruntjs.com/), [Nodemailer](http://www.nodemailer.com/), [Yeoman](http://yeoman.io/) and others.
* Faster than [node-iconv](https://github.com/bnoordhuis/node-iconv) (see below for performance comparison).
* Intuitive encode/decode API, including Streaming support.
* In-browser usage via [browserify](https://github.com/substack/node-browserify) or [webpack](https://webpack.js.org/) (~180kb gzip compressed with Buffer shim included).
* Typescript [type definition file](https://github.com/ashtuchkin/iconv-lite/blob/master/lib/index.d.ts) included.
* React Native is supported (need to install `stream` module to enable Streaming API).
## Usage
### Basic API
```javascript
var iconv = require('iconv-lite');
// Convert from an encoded buffer to a js string.
str = iconv.decode(Buffer.from([0x68, 0x65, 0x6c, 0x6c, 0x6f]), 'win1251');
// Convert from a js string to an encoded buffer.
buf = iconv.encode("Sample input string", 'win1251');
// Check if encoding is supported
iconv.encodingExists("us-ascii")
```
### Streaming API
```javascript
// Decode stream (from binary data stream to js strings)
http.createServer(function(req, res) {
var converterStream = iconv.decodeStream('win1251');
req.pipe(converterStream);
converterStream.on('data', function(str) {
console.log(str); // Do something with decoded strings, chunk-by-chunk.
});
});
// Convert encoding streaming example
fs.createReadStream('file-in-win1251.txt')
.pipe(iconv.decodeStream('win1251'))
.pipe(iconv.encodeStream('ucs2'))
.pipe(fs.createWriteStream('file-in-ucs2.txt'));
// Sugar: all encode/decode streams have .collect(cb) method to accumulate data.
http.createServer(function(req, res) {
req.pipe(iconv.decodeStream('win1251')).collect(function(err, body) {
assert(typeof body == 'string');
console.log(body); // full request body string
});
});
```
## Supported encodings
* All node.js native encodings: utf8, ucs2 / utf16-le, ascii, binary, base64, hex.
* Additional unicode encodings: utf16, utf16-be, utf-7, utf-7-imap, utf32, utf32-le, and utf32-be.
* All widespread singlebyte encodings: Windows 125x family, ISO-8859 family,
IBM/DOS codepages, Macintosh family, KOI8 family, all others supported by iconv library.
Aliases like 'latin1', 'us-ascii' also supported.
* All widespread multibyte encodings: CP932, CP936, CP949, CP950, GB2312, GBK, GB18030, Big5, Shift_JIS, EUC-JP.
See [all supported encodings on wiki](https://github.com/ashtuchkin/iconv-lite/wiki/Supported-Encodings).
Most singlebyte encodings are generated automatically from [node-iconv](https://github.com/bnoordhuis/node-iconv). Thank you Ben Noordhuis and libiconv authors!
Multibyte encodings are generated from [Unicode.org mappings](http://www.unicode.org/Public/MAPPINGS/) and [WHATWG Encoding Standard mappings](http://encoding.spec.whatwg.org/). Thank you, respective authors!
## Encoding/decoding speed
Comparison with node-iconv module (1000x256kb, on MacBook Pro, Core i5/2.6 GHz, Node v0.12.0).
Note: your results may vary, so please always check on your hardware.
operation iconv@2.1.4 iconv-lite@0.4.7
----------------------------------------------------------
encode('win1251') ~96 Mb/s ~320 Mb/s
decode('win1251') ~95 Mb/s ~246 Mb/s
## BOM handling
* Decoding: BOM is stripped by default, unless overridden by passing `stripBOM: false` in options
(f.ex. `iconv.decode(buf, enc, {stripBOM: false})`).
A callback might also be given as a `stripBOM` parameter - it'll be called if BOM character was actually found.
* If you want to detect UTF-8 BOM when decoding other encodings, use [node-autodetect-decoder-stream](https://github.com/danielgindi/node-autodetect-decoder-stream) module.
* Encoding: No BOM added, unless overridden by `addBOM: true` option.
## UTF-16 Encodings
This library supports UTF-16LE, UTF-16BE and UTF-16 encodings. First two are straightforward, but UTF-16 is trying to be
smart about endianness in the following ways:
* Decoding: uses BOM and 'spaces heuristic' to determine input endianness. Default is UTF-16LE, but can be
overridden with `defaultEncoding: 'utf-16be'` option. Strips BOM unless `stripBOM: false`.
* Encoding: uses UTF-16LE and writes BOM by default. Use `addBOM: false` to override.
## UTF-32 Encodings
This library supports UTF-32LE, UTF-32BE and UTF-32 encodings. Like the UTF-16 encoding above, UTF-32 defaults to UTF-32LE, but uses BOM and 'spaces heuristics' to determine input endianness.
* The default of UTF-32LE can be overridden with the `defaultEncoding: 'utf-32be'` option. Strips BOM unless `stripBOM: false`.
* Encoding: uses UTF-32LE and writes BOM by default. Use `addBOM: false` to override. (`defaultEncoding: 'utf-32be'` can also be used here to change encoding.)
## Other notes
When decoding, be sure to supply a Buffer to decode() method, otherwise [bad things usually happen](https://github.com/ashtuchkin/iconv-lite/wiki/Use-Buffers-when-decoding).
Untranslatable characters are set to or ?. No transliteration is currently supported.
Node versions 0.10.31 and 0.11.13 are buggy, don't use them (see [#65](https://github.com/ashtuchkin/iconv-lite/issues/65), [#77](https://github.com/ashtuchkin/iconv-lite/issues/77)).
## Testing
```sh
git clone git@github.com:ashtuchkin/iconv-lite.git
cd iconv-lite
npm install
npm test
# To view performance:
npm run test:performance
# To view test coverage:
npm run test:cov
open coverage/index.html
```
[npm-downloads-image]: https://badgen.net/npm/dm/iconv-lite
[npm-downloads-url]: https://npmcharts.com/compare/iconv-lite?minimal=true
[npm-url]: https://npmjs.org/package/iconv-lite
[npm-version-image]: https://badgen.net/npm/v/iconv-lite
[npm-install-size-image]: https://badgen.net/packagephobia/install/iconv-lite
[npm-install-size-url]: https://packagephobia.com/result?p=iconv-lite
[license-image]: https://img.shields.io/npm/l/iconv-lite.svg
[license-url]: https://github.com/ashtuchkin/iconv-lite/blob/HEAD/LICENSE
@@ -0,0 +1,532 @@
"use strict"
var Buffer = require("safer-buffer").Buffer
// Multibyte codec. In this scheme, a character is represented by 1 or more bytes.
// Our codec supports UTF-16 surrogates, extensions for GB18030 and unicode sequences.
// To save memory and loading time, we read table files only when requested.
exports._dbcs = DBCSCodec
var UNASSIGNED = -1
var GB18030_CODE = -2
var SEQ_START = -10
var NODE_START = -1000
var UNASSIGNED_NODE = new Array(0x100)
var DEF_CHAR = -1
for (var i = 0; i < 0x100; i++) { UNASSIGNED_NODE[i] = UNASSIGNED }
// Class DBCSCodec reads and initializes mapping tables.
function DBCSCodec (codecOptions, iconv) {
this.encodingName = codecOptions.encodingName
if (!codecOptions) { throw new Error("DBCS codec is called without the data.") }
if (!codecOptions.table) { throw new Error("Encoding '" + this.encodingName + "' has no data.") }
// Load tables.
var mappingTable = codecOptions.table()
// Decode tables: MBCS -> Unicode.
// decodeTables is a trie, encoded as an array of arrays of integers. Internal arrays are trie nodes and all have len = 256.
// Trie root is decodeTables[0].
// Values: >= 0 -> unicode character code. can be > 0xFFFF
// == UNASSIGNED -> unknown/unassigned sequence.
// == GB18030_CODE -> this is the end of a GB18030 4-byte sequence.
// <= NODE_START -> index of the next node in our trie to process next byte.
// <= SEQ_START -> index of the start of a character code sequence, in decodeTableSeq.
this.decodeTables = []
this.decodeTables[0] = UNASSIGNED_NODE.slice(0) // Create root node.
// Sometimes a MBCS char corresponds to a sequence of unicode chars. We store them as arrays of integers here.
this.decodeTableSeq = []
// Actual mapping tables consist of chunks. Use them to fill up decode tables.
for (var i = 0; i < mappingTable.length; i++) { this._addDecodeChunk(mappingTable[i]) }
// Load & create GB18030 tables when needed.
if (typeof codecOptions.gb18030 === "function") {
this.gb18030 = codecOptions.gb18030() // Load GB18030 ranges.
// Add GB18030 common decode nodes.
var commonThirdByteNodeIdx = this.decodeTables.length
this.decodeTables.push(UNASSIGNED_NODE.slice(0))
var commonFourthByteNodeIdx = this.decodeTables.length
this.decodeTables.push(UNASSIGNED_NODE.slice(0))
// Fill out the tree
var firstByteNode = this.decodeTables[0]
for (var i = 0x81; i <= 0xFE; i++) {
var secondByteNode = this.decodeTables[NODE_START - firstByteNode[i]]
for (var j = 0x30; j <= 0x39; j++) {
if (secondByteNode[j] === UNASSIGNED) {
secondByteNode[j] = NODE_START - commonThirdByteNodeIdx
} else if (secondByteNode[j] > NODE_START) {
throw new Error("gb18030 decode tables conflict at byte 2")
}
var thirdByteNode = this.decodeTables[NODE_START - secondByteNode[j]]
for (var k = 0x81; k <= 0xFE; k++) {
if (thirdByteNode[k] === UNASSIGNED) {
thirdByteNode[k] = NODE_START - commonFourthByteNodeIdx
} else if (thirdByteNode[k] === NODE_START - commonFourthByteNodeIdx) {
continue
} else if (thirdByteNode[k] > NODE_START) {
throw new Error("gb18030 decode tables conflict at byte 3")
}
var fourthByteNode = this.decodeTables[NODE_START - thirdByteNode[k]]
for (var l = 0x30; l <= 0x39; l++) {
if (fourthByteNode[l] === UNASSIGNED) { fourthByteNode[l] = GB18030_CODE }
}
}
}
}
}
this.defaultCharUnicode = iconv.defaultCharUnicode
// Encode tables: Unicode -> DBCS.
// `encodeTable` is array mapping from unicode char to encoded char. All its values are integers for performance.
// Because it can be sparse, it is represented as array of buckets by 256 chars each. Bucket can be null.
// Values: >= 0 -> it is a normal char. Write the value (if <=256 then 1 byte, if <=65536 then 2 bytes, etc.).
// == UNASSIGNED -> no conversion found. Output a default char.
// <= SEQ_START -> it's an index in encodeTableSeq, see below. The character starts a sequence.
this.encodeTable = []
// `encodeTableSeq` is used when a sequence of unicode characters is encoded as a single code. We use a tree of
// objects where keys correspond to characters in sequence and leafs are the encoded dbcs values. A special DEF_CHAR key
// means end of sequence (needed when one sequence is a strict subsequence of another).
// Objects are kept separately from encodeTable to increase performance.
this.encodeTableSeq = []
// Some chars can be decoded, but need not be encoded.
var skipEncodeChars = {}
if (codecOptions.encodeSkipVals) {
for (var i = 0; i < codecOptions.encodeSkipVals.length; i++) {
var val = codecOptions.encodeSkipVals[i]
if (typeof val === "number") { skipEncodeChars[val] = true } else {
for (var j = val.from; j <= val.to; j++) { skipEncodeChars[j] = true }
}
}
}
// Use decode trie to recursively fill out encode tables.
this._fillEncodeTable(0, 0, skipEncodeChars)
// Add more encoding pairs when needed.
if (codecOptions.encodeAdd) {
for (var uChar in codecOptions.encodeAdd) {
if (Object.prototype.hasOwnProperty.call(codecOptions.encodeAdd, uChar)) { this._setEncodeChar(uChar.charCodeAt(0), codecOptions.encodeAdd[uChar]) }
}
}
this.defCharSB = this.encodeTable[0][iconv.defaultCharSingleByte.charCodeAt(0)]
if (this.defCharSB === UNASSIGNED) this.defCharSB = this.encodeTable[0]["?"]
if (this.defCharSB === UNASSIGNED) this.defCharSB = "?".charCodeAt(0)
}
DBCSCodec.prototype.encoder = DBCSEncoder
DBCSCodec.prototype.decoder = DBCSDecoder
// Decoder helpers
DBCSCodec.prototype._getDecodeTrieNode = function (addr) {
var bytes = []
for (; addr > 0; addr >>>= 8) { bytes.push(addr & 0xFF) }
if (bytes.length == 0) { bytes.push(0) }
var node = this.decodeTables[0]
for (var i = bytes.length - 1; i > 0; i--) { // Traverse nodes deeper into the trie.
var val = node[bytes[i]]
if (val == UNASSIGNED) { // Create new node.
node[bytes[i]] = NODE_START - this.decodeTables.length
this.decodeTables.push(node = UNASSIGNED_NODE.slice(0))
} else if (val <= NODE_START) { // Existing node.
node = this.decodeTables[NODE_START - val]
} else { throw new Error("Overwrite byte in " + this.encodingName + ", addr: " + addr.toString(16)) }
}
return node
}
DBCSCodec.prototype._addDecodeChunk = function (chunk) {
// First element of chunk is the hex mbcs code where we start.
var curAddr = parseInt(chunk[0], 16)
// Choose the decoding node where we'll write our chars.
var writeTable = this._getDecodeTrieNode(curAddr)
curAddr = curAddr & 0xFF
// Write all other elements of the chunk to the table.
for (var k = 1; k < chunk.length; k++) {
var part = chunk[k]
if (typeof part === "string") { // String, write as-is.
for (var l = 0; l < part.length;) {
var code = part.charCodeAt(l++)
if (code >= 0xD800 && code < 0xDC00) { // Decode surrogate
var codeTrail = part.charCodeAt(l++)
if (codeTrail >= 0xDC00 && codeTrail < 0xE000) { writeTable[curAddr++] = 0x10000 + (code - 0xD800) * 0x400 + (codeTrail - 0xDC00) } else { throw new Error("Incorrect surrogate pair in " + this.encodingName + " at chunk " + chunk[0]) }
} else if (code > 0x0FF0 && code <= 0x0FFF) { // Character sequence (our own encoding used)
var len = 0xFFF - code + 2
var seq = []
for (var m = 0; m < len; m++) { seq.push(part.charCodeAt(l++)) } // Simple variation: don't support surrogates or subsequences in seq.
writeTable[curAddr++] = SEQ_START - this.decodeTableSeq.length
this.decodeTableSeq.push(seq)
} else { writeTable[curAddr++] = code } // Basic char
}
} else if (typeof part === "number") { // Integer, meaning increasing sequence starting with prev character.
var charCode = writeTable[curAddr - 1] + 1
for (var l = 0; l < part; l++) { writeTable[curAddr++] = charCode++ }
} else { throw new Error("Incorrect type '" + typeof part + "' given in " + this.encodingName + " at chunk " + chunk[0]) }
}
if (curAddr > 0xFF) { throw new Error("Incorrect chunk in " + this.encodingName + " at addr " + chunk[0] + ": too long" + curAddr) }
}
// Encoder helpers
DBCSCodec.prototype._getEncodeBucket = function (uCode) {
var high = uCode >> 8 // This could be > 0xFF because of astral characters.
if (this.encodeTable[high] === undefined) {
this.encodeTable[high] = UNASSIGNED_NODE.slice(0)
} // Create bucket on demand.
return this.encodeTable[high]
}
DBCSCodec.prototype._setEncodeChar = function (uCode, dbcsCode) {
var bucket = this._getEncodeBucket(uCode)
var low = uCode & 0xFF
if (bucket[low] <= SEQ_START) { this.encodeTableSeq[SEQ_START - bucket[low]][DEF_CHAR] = dbcsCode } // There's already a sequence, set a single-char subsequence of it.
else if (bucket[low] == UNASSIGNED) { bucket[low] = dbcsCode }
}
DBCSCodec.prototype._setEncodeSequence = function (seq, dbcsCode) {
// Get the root of character tree according to first character of the sequence.
var uCode = seq[0]
var bucket = this._getEncodeBucket(uCode)
var low = uCode & 0xFF
var node
if (bucket[low] <= SEQ_START) {
// There's already a sequence with - use it.
node = this.encodeTableSeq[SEQ_START - bucket[low]]
} else {
// There was no sequence object - allocate a new one.
node = {}
if (bucket[low] !== UNASSIGNED) node[DEF_CHAR] = bucket[low] // If a char was set before - make it a single-char subsequence.
bucket[low] = SEQ_START - this.encodeTableSeq.length
this.encodeTableSeq.push(node)
}
// Traverse the character tree, allocating new nodes as needed.
for (var j = 1; j < seq.length - 1; j++) {
var oldVal = node[uCode]
if (typeof oldVal === "object") { node = oldVal } else {
node = node[uCode] = {}
if (oldVal !== undefined) { node[DEF_CHAR] = oldVal }
}
}
// Set the leaf to given dbcsCode.
uCode = seq[seq.length - 1]
node[uCode] = dbcsCode
}
DBCSCodec.prototype._fillEncodeTable = function (nodeIdx, prefix, skipEncodeChars) {
var node = this.decodeTables[nodeIdx]
var hasValues = false
var subNodeEmpty = {}
for (var i = 0; i < 0x100; i++) {
var uCode = node[i]
var mbCode = prefix + i
if (skipEncodeChars[mbCode]) { continue }
if (uCode >= 0) {
this._setEncodeChar(uCode, mbCode)
hasValues = true
} else if (uCode <= NODE_START) {
var subNodeIdx = NODE_START - uCode
if (!subNodeEmpty[subNodeIdx]) { // Skip empty subtrees (they are too large in gb18030).
var newPrefix = (mbCode << 8) >>> 0 // NOTE: '>>> 0' keeps 32-bit num positive.
if (this._fillEncodeTable(subNodeIdx, newPrefix, skipEncodeChars)) { hasValues = true } else { subNodeEmpty[subNodeIdx] = true }
}
} else if (uCode <= SEQ_START) {
this._setEncodeSequence(this.decodeTableSeq[SEQ_START - uCode], mbCode)
hasValues = true
}
}
return hasValues
}
// == Encoder ==================================================================
function DBCSEncoder (options, codec) {
// Encoder state
this.leadSurrogate = -1
this.seqObj = undefined
// Static data
this.encodeTable = codec.encodeTable
this.encodeTableSeq = codec.encodeTableSeq
this.defaultCharSingleByte = codec.defCharSB
this.gb18030 = codec.gb18030
}
DBCSEncoder.prototype.write = function (str) {
var newBuf = Buffer.alloc(str.length * (this.gb18030 ? 4 : 3))
var leadSurrogate = this.leadSurrogate
var seqObj = this.seqObj
var nextChar = -1
var i = 0; var j = 0
while (true) {
// 0. Get next character.
if (nextChar === -1) {
if (i == str.length) break
var uCode = str.charCodeAt(i++)
} else {
var uCode = nextChar
nextChar = -1
}
// 1. Handle surrogates.
if (uCode >= 0xD800 && uCode < 0xE000) { // Char is one of surrogates.
if (uCode < 0xDC00) { // We've got lead surrogate.
if (leadSurrogate === -1) {
leadSurrogate = uCode
continue
} else {
leadSurrogate = uCode
// Double lead surrogate found.
uCode = UNASSIGNED
}
} else { // We've got trail surrogate.
if (leadSurrogate !== -1) {
uCode = 0x10000 + (leadSurrogate - 0xD800) * 0x400 + (uCode - 0xDC00)
leadSurrogate = -1
} else {
// Incomplete surrogate pair - only trail surrogate found.
uCode = UNASSIGNED
}
}
} else if (leadSurrogate !== -1) {
// Incomplete surrogate pair - only lead surrogate found.
nextChar = uCode; uCode = UNASSIGNED // Write an error, then current char.
leadSurrogate = -1
}
// 2. Convert uCode character.
var dbcsCode = UNASSIGNED
if (seqObj !== undefined && uCode != UNASSIGNED) { // We are in the middle of the sequence
var resCode = seqObj[uCode]
if (typeof resCode === "object") { // Sequence continues.
seqObj = resCode
continue
} else if (typeof resCode === "number") { // Sequence finished. Write it.
dbcsCode = resCode
} else if (resCode == undefined) { // Current character is not part of the sequence.
// Try default character for this sequence
resCode = seqObj[DEF_CHAR]
if (resCode !== undefined) {
dbcsCode = resCode // Found. Write it.
nextChar = uCode // Current character will be written too in the next iteration.
} else {
// TODO: What if we have no default? (resCode == undefined)
// Then, we should write first char of the sequence as-is and try the rest recursively.
// Didn't do it for now because no encoding has this situation yet.
// Currently, just skip the sequence and write current char.
}
}
seqObj = undefined
} else if (uCode >= 0) { // Regular character
var subtable = this.encodeTable[uCode >> 8]
if (subtable !== undefined) { dbcsCode = subtable[uCode & 0xFF] }
if (dbcsCode <= SEQ_START) { // Sequence start
seqObj = this.encodeTableSeq[SEQ_START - dbcsCode]
continue
}
if (dbcsCode == UNASSIGNED && this.gb18030) {
// Use GB18030 algorithm to find character(s) to write.
var idx = findIdx(this.gb18030.uChars, uCode)
if (idx != -1) {
var dbcsCode = this.gb18030.gbChars[idx] + (uCode - this.gb18030.uChars[idx])
newBuf[j++] = 0x81 + Math.floor(dbcsCode / 12600); dbcsCode = dbcsCode % 12600
newBuf[j++] = 0x30 + Math.floor(dbcsCode / 1260); dbcsCode = dbcsCode % 1260
newBuf[j++] = 0x81 + Math.floor(dbcsCode / 10); dbcsCode = dbcsCode % 10
newBuf[j++] = 0x30 + dbcsCode
continue
}
}
}
// 3. Write dbcsCode character.
if (dbcsCode === UNASSIGNED) { dbcsCode = this.defaultCharSingleByte }
if (dbcsCode < 0x100) {
newBuf[j++] = dbcsCode
} else if (dbcsCode < 0x10000) {
newBuf[j++] = dbcsCode >> 8 // high byte
newBuf[j++] = dbcsCode & 0xFF // low byte
} else if (dbcsCode < 0x1000000) {
newBuf[j++] = dbcsCode >> 16
newBuf[j++] = (dbcsCode >> 8) & 0xFF
newBuf[j++] = dbcsCode & 0xFF
} else {
newBuf[j++] = dbcsCode >>> 24
newBuf[j++] = (dbcsCode >>> 16) & 0xFF
newBuf[j++] = (dbcsCode >>> 8) & 0xFF
newBuf[j++] = dbcsCode & 0xFF
}
}
this.seqObj = seqObj
this.leadSurrogate = leadSurrogate
return newBuf.slice(0, j)
}
DBCSEncoder.prototype.end = function () {
if (this.leadSurrogate === -1 && this.seqObj === undefined) { return } // All clean. Most often case.
var newBuf = Buffer.alloc(10); var j = 0
if (this.seqObj) { // We're in the sequence.
var dbcsCode = this.seqObj[DEF_CHAR]
if (dbcsCode !== undefined) { // Write beginning of the sequence.
if (dbcsCode < 0x100) {
newBuf[j++] = dbcsCode
} else {
newBuf[j++] = dbcsCode >> 8 // high byte
newBuf[j++] = dbcsCode & 0xFF // low byte
}
} else {
// See todo above.
}
this.seqObj = undefined
}
if (this.leadSurrogate !== -1) {
// Incomplete surrogate pair - only lead surrogate found.
newBuf[j++] = this.defaultCharSingleByte
this.leadSurrogate = -1
}
return newBuf.slice(0, j)
}
// Export for testing
DBCSEncoder.prototype.findIdx = findIdx
// == Decoder ==================================================================
function DBCSDecoder (options, codec) {
// Decoder state
this.nodeIdx = 0
this.prevBytes = []
// Static data
this.decodeTables = codec.decodeTables
this.decodeTableSeq = codec.decodeTableSeq
this.defaultCharUnicode = codec.defaultCharUnicode
this.gb18030 = codec.gb18030
}
DBCSDecoder.prototype.write = function (buf) {
var newBuf = Buffer.alloc(buf.length * 2)
var nodeIdx = this.nodeIdx
var prevBytes = this.prevBytes; var prevOffset = this.prevBytes.length
var seqStart = -this.prevBytes.length // idx of the start of current parsed sequence.
var uCode
for (var i = 0, j = 0; i < buf.length; i++) {
var curByte = (i >= 0) ? buf[i] : prevBytes[i + prevOffset]
// Lookup in current trie node.
var uCode = this.decodeTables[nodeIdx][curByte]
if (uCode >= 0) {
// Normal character, just use it.
} else if (uCode === UNASSIGNED) { // Unknown char.
// TODO: Callback with seq.
uCode = this.defaultCharUnicode.charCodeAt(0)
i = seqStart // Skip one byte ('i' will be incremented by the for loop) and try to parse again.
} else if (uCode === GB18030_CODE) {
if (i >= 3) {
var ptr = (buf[i - 3] - 0x81) * 12600 + (buf[i - 2] - 0x30) * 1260 + (buf[i - 1] - 0x81) * 10 + (curByte - 0x30)
} else {
var ptr = (prevBytes[i - 3 + prevOffset] - 0x81) * 12600 +
(((i - 2 >= 0) ? buf[i - 2] : prevBytes[i - 2 + prevOffset]) - 0x30) * 1260 +
(((i - 1 >= 0) ? buf[i - 1] : prevBytes[i - 1 + prevOffset]) - 0x81) * 10 +
(curByte - 0x30)
}
var idx = findIdx(this.gb18030.gbChars, ptr)
uCode = this.gb18030.uChars[idx] + ptr - this.gb18030.gbChars[idx]
} else if (uCode <= NODE_START) { // Go to next trie node.
nodeIdx = NODE_START - uCode
continue
} else if (uCode <= SEQ_START) { // Output a sequence of chars.
var seq = this.decodeTableSeq[SEQ_START - uCode]
for (var k = 0; k < seq.length - 1; k++) {
uCode = seq[k]
newBuf[j++] = uCode & 0xFF
newBuf[j++] = uCode >> 8
}
uCode = seq[seq.length - 1]
} else { throw new Error("iconv-lite internal error: invalid decoding table value " + uCode + " at " + nodeIdx + "/" + curByte) }
// Write the character to buffer, handling higher planes using surrogate pair.
if (uCode >= 0x10000) {
uCode -= 0x10000
var uCodeLead = 0xD800 | (uCode >> 10)
newBuf[j++] = uCodeLead & 0xFF
newBuf[j++] = uCodeLead >> 8
uCode = 0xDC00 | (uCode & 0x3FF)
}
newBuf[j++] = uCode & 0xFF
newBuf[j++] = uCode >> 8
// Reset trie node.
nodeIdx = 0; seqStart = i + 1
}
this.nodeIdx = nodeIdx
this.prevBytes = (seqStart >= 0)
? Array.prototype.slice.call(buf, seqStart)
: prevBytes.slice(seqStart + prevOffset).concat(Array.prototype.slice.call(buf))
return newBuf.slice(0, j).toString("ucs2")
}
DBCSDecoder.prototype.end = function () {
var ret = ""
// Try to parse all remaining chars.
while (this.prevBytes.length > 0) {
// Skip 1 character in the buffer.
ret += this.defaultCharUnicode
var bytesArr = this.prevBytes.slice(1)
// Parse remaining as usual.
this.prevBytes = []
this.nodeIdx = 0
if (bytesArr.length > 0) { ret += this.write(bytesArr) }
}
this.prevBytes = []
this.nodeIdx = 0
return ret
}
// Binary search for GB18030. Returns largest i such that table[i] <= val.
function findIdx (table, val) {
if (table[0] > val) { return -1 }
var l = 0; var r = table.length
while (l < r - 1) { // always table[l] <= val < table[r]
var mid = l + ((r - l + 1) >> 1)
if (table[mid] <= val) { l = mid } else { r = mid }
}
return l
}
@@ -0,0 +1,185 @@
"use strict"
// Description of supported double byte encodings and aliases.
// Tables are not require()-d until they are needed to speed up library load.
// require()-s are direct to support Browserify.
module.exports = {
// == Japanese/ShiftJIS ====================================================
// All japanese encodings are based on JIS X set of standards:
// JIS X 0201 - Single-byte encoding of ASCII + ¥ + Kana chars at 0xA1-0xDF.
// JIS X 0208 - Main set of 6879 characters, placed in 94x94 plane, to be encoded by 2 bytes.
// Has several variations in 1978, 1983, 1990 and 1997.
// JIS X 0212 - Supplementary plane of 6067 chars in 94x94 plane. 1990. Effectively dead.
// JIS X 0213 - Extension and modern replacement of 0208 and 0212. Total chars: 11233.
// 2 planes, first is superset of 0208, second - revised 0212.
// Introduced in 2000, revised 2004. Some characters are in Unicode Plane 2 (0x2xxxx)
// Byte encodings are:
// * Shift_JIS: Compatible with 0201, uses not defined chars in top half as lead bytes for double-byte
// encoding of 0208. Lead byte ranges: 0x81-0x9F, 0xE0-0xEF; Trail byte ranges: 0x40-0x7E, 0x80-0x9E, 0x9F-0xFC.
// Windows CP932 is a superset of Shift_JIS. Some companies added more chars, notably KDDI.
// * EUC-JP: Up to 3 bytes per character. Used mostly on *nixes.
// 0x00-0x7F - lower part of 0201
// 0x8E, 0xA1-0xDF - upper part of 0201
// (0xA1-0xFE)x2 - 0208 plane (94x94).
// 0x8F, (0xA1-0xFE)x2 - 0212 plane (94x94).
// * JIS X 208: 7-bit, direct encoding of 0208. Byte ranges: 0x21-0x7E (94 values). Uncommon.
// Used as-is in ISO2022 family.
// * ISO2022-JP: Stateful encoding, with escape sequences to switch between ASCII,
// 0201-1976 Roman, 0208-1978, 0208-1983.
// * ISO2022-JP-1: Adds esc seq for 0212-1990.
// * ISO2022-JP-2: Adds esc seq for GB2313-1980, KSX1001-1992, ISO8859-1, ISO8859-7.
// * ISO2022-JP-3: Adds esc seq for 0201-1976 Kana set, 0213-2000 Planes 1, 2.
// * ISO2022-JP-2004: Adds 0213-2004 Plane 1.
//
// After JIS X 0213 appeared, Shift_JIS-2004, EUC-JISX0213 and ISO2022-JP-2004 followed, with just changing the planes.
//
// Overall, it seems that it's a mess :( http://www8.plala.or.jp/tkubota1/unicode-symbols-map2.html
shiftjis: {
type: "_dbcs",
table: function () { return require("./tables/shiftjis.json") },
encodeAdd: { "\u00a5": 0x5C, "\u203E": 0x7E },
encodeSkipVals: [{ from: 0xED40, to: 0xF940 }]
},
csshiftjis: "shiftjis",
mskanji: "shiftjis",
sjis: "shiftjis",
windows31j: "shiftjis",
ms31j: "shiftjis",
xsjis: "shiftjis",
windows932: "shiftjis",
ms932: "shiftjis",
932: "shiftjis",
cp932: "shiftjis",
eucjp: {
type: "_dbcs",
table: function () { return require("./tables/eucjp.json") },
encodeAdd: { "\u00a5": 0x5C, "\u203E": 0x7E }
},
// TODO: KDDI extension to Shift_JIS
// TODO: IBM CCSID 942 = CP932, but F0-F9 custom chars and other char changes.
// TODO: IBM CCSID 943 = Shift_JIS = CP932 with original Shift_JIS lower 128 chars.
// == Chinese/GBK ==========================================================
// http://en.wikipedia.org/wiki/GBK
// We mostly implement W3C recommendation: https://www.w3.org/TR/encoding/#gbk-encoder
// Oldest GB2312 (1981, ~7600 chars) is a subset of CP936
gb2312: "cp936",
gb231280: "cp936",
gb23121980: "cp936",
csgb2312: "cp936",
csiso58gb231280: "cp936",
euccn: "cp936",
// Microsoft's CP936 is a subset and approximation of GBK.
windows936: "cp936",
ms936: "cp936",
936: "cp936",
cp936: {
type: "_dbcs",
table: function () { return require("./tables/cp936.json") }
},
// GBK (~22000 chars) is an extension of CP936 that added user-mapped chars and some other.
gbk: {
type: "_dbcs",
table: function () { return require("./tables/cp936.json").concat(require("./tables/gbk-added.json")) }
},
xgbk: "gbk",
isoir58: "gbk",
// GB18030 is an algorithmic extension of GBK.
// Main source: https://www.w3.org/TR/encoding/#gbk-encoder
// http://icu-project.org/docs/papers/gb18030.html
// http://source.icu-project.org/repos/icu/data/trunk/charset/data/xml/gb-18030-2000.xml
// http://www.khngai.com/chinese/charmap/tblgbk.php?page=0
gb18030: {
type: "_dbcs",
table: function () { return require("./tables/cp936.json").concat(require("./tables/gbk-added.json")) },
gb18030: function () { return require("./tables/gb18030-ranges.json") },
encodeSkipVals: [0x80],
encodeAdd: { "€": 0xA2E3 }
},
chinese: "gb18030",
// == Korean ===============================================================
// EUC-KR, KS_C_5601 and KS X 1001 are exactly the same.
windows949: "cp949",
ms949: "cp949",
949: "cp949",
cp949: {
type: "_dbcs",
table: function () { return require("./tables/cp949.json") }
},
cseuckr: "cp949",
csksc56011987: "cp949",
euckr: "cp949",
isoir149: "cp949",
korean: "cp949",
ksc56011987: "cp949",
ksc56011989: "cp949",
ksc5601: "cp949",
// == Big5/Taiwan/Hong Kong ================================================
// There are lots of tables for Big5 and cp950. Please see the following links for history:
// http://moztw.org/docs/big5/ http://www.haible.de/bruno/charsets/conversion-tables/Big5.html
// Variations, in roughly number of defined chars:
// * Windows CP 950: Microsoft variant of Big5. Canonical: http://www.unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP950.TXT
// * Windows CP 951: Microsoft variant of Big5-HKSCS-2001. Seems to be never public. http://me.abelcheung.org/articles/research/what-is-cp951/
// * Big5-2003 (Taiwan standard) almost superset of cp950.
// * Unicode-at-on (UAO) / Mozilla 1.8. Falling out of use on the Web. Not supported by other browsers.
// * Big5-HKSCS (-2001, -2004, -2008). Hong Kong standard.
// many unicode code points moved from PUA to Supplementary plane (U+2XXXX) over the years.
// Plus, it has 4 combining sequences.
// Seems that Mozilla refused to support it for 10 yrs. https://bugzilla.mozilla.org/show_bug.cgi?id=162431 https://bugzilla.mozilla.org/show_bug.cgi?id=310299
// because big5-hkscs is the only encoding to include astral characters in non-algorithmic way.
// Implementations are not consistent within browsers; sometimes labeled as just big5.
// MS Internet Explorer switches from big5 to big5-hkscs when a patch applied.
// Great discussion & recap of what's going on https://bugzilla.mozilla.org/show_bug.cgi?id=912470#c31
// In the encoder, it might make sense to support encoding old PUA mappings to Big5 bytes seq-s.
// Official spec: http://www.ogcio.gov.hk/en/business/tech_promotion/ccli/terms/doc/2003cmp_2008.txt
// http://www.ogcio.gov.hk/tc/business/tech_promotion/ccli/terms/doc/hkscs-2008-big5-iso.txt
//
// Current understanding of how to deal with Big5(-HKSCS) is in the Encoding Standard, http://encoding.spec.whatwg.org/#big5-encoder
// Unicode mapping (http://www.unicode.org/Public/MAPPINGS/OBSOLETE/EASTASIA/OTHER/BIG5.TXT) is said to be wrong.
windows950: "cp950",
ms950: "cp950",
950: "cp950",
cp950: {
type: "_dbcs",
table: function () { return require("./tables/cp950.json") }
},
// Big5 has many variations and is an extension of cp950. We use Encoding Standard's as a consensus.
big5: "big5hkscs",
big5hkscs: {
type: "_dbcs",
table: function () { return require("./tables/cp950.json").concat(require("./tables/big5-added.json")) },
encodeSkipVals: [
// Although Encoding Standard says we should avoid encoding to HKSCS area (See Step 1 of
// https://encoding.spec.whatwg.org/#index-big5-pointer), we still do it to increase compatibility with ICU.
// But if a single unicode point can be encoded both as HKSCS and regular Big5, we prefer the latter.
0x8e69, 0x8e6f, 0x8e7e, 0x8eab, 0x8eb4, 0x8ecd, 0x8ed0, 0x8f57, 0x8f69, 0x8f6e, 0x8fcb, 0x8ffe,
0x906d, 0x907a, 0x90c4, 0x90dc, 0x90f1, 0x91bf, 0x92af, 0x92b0, 0x92b1, 0x92b2, 0x92d1, 0x9447, 0x94ca,
0x95d9, 0x96fc, 0x9975, 0x9b76, 0x9b78, 0x9b7b, 0x9bc6, 0x9bde, 0x9bec, 0x9bf6, 0x9c42, 0x9c53, 0x9c62,
0x9c68, 0x9c6b, 0x9c77, 0x9cbc, 0x9cbd, 0x9cd0, 0x9d57, 0x9d5a, 0x9dc4, 0x9def, 0x9dfb, 0x9ea9, 0x9eef,
0x9efd, 0x9f60, 0x9fcb, 0xa077, 0xa0dc, 0xa0df, 0x8fcc, 0x92c8, 0x9644, 0x96ed,
// Step 2 of https://encoding.spec.whatwg.org/#index-big5-pointer: Use last pointer for U+2550, U+255E, U+2561, U+256A, U+5341, or U+5345
0xa2a4, 0xa2a5, 0xa2a7, 0xa2a6, 0xa2cc, 0xa2ce
]
},
cnbig5: "big5hkscs",
csbig5: "big5hkscs",
xxbig5: "big5hkscs"
}
+23
View File
@@ -0,0 +1,23 @@
"use strict"
var mergeModules = require("../lib/helpers/merge-exports")
// Update this array if you add/rename/remove files in this directory.
// We support Browserify by skipping automatic module discovery and requiring modules directly.
var modules = [
require("./internal"),
require("./utf32"),
require("./utf16"),
require("./utf7"),
require("./sbcs-codec"),
require("./sbcs-data"),
require("./sbcs-data-generated"),
require("./dbcs-codec"),
require("./dbcs-data")
]
// Put all encoding/alias/codec definitions to single object and export it.
for (var i = 0; i < modules.length; i++) {
var module = modules[i]
mergeModules(exports, module)
}
@@ -0,0 +1,218 @@
"use strict"
var Buffer = require("safer-buffer").Buffer
// Export Node.js internal encodings.
module.exports = {
// Encodings
utf8: { type: "_internal", bomAware: true },
cesu8: { type: "_internal", bomAware: true },
unicode11utf8: "utf8",
ucs2: { type: "_internal", bomAware: true },
utf16le: "ucs2",
binary: { type: "_internal" },
base64: { type: "_internal" },
hex: { type: "_internal" },
// Codec.
_internal: InternalCodec
}
// ------------------------------------------------------------------------------
function InternalCodec (codecOptions, iconv) {
this.enc = codecOptions.encodingName
this.bomAware = codecOptions.bomAware
if (this.enc === "base64") { this.encoder = InternalEncoderBase64 } else if (this.enc === "utf8") { this.encoder = InternalEncoderUtf8 } else if (this.enc === "cesu8") {
this.enc = "utf8" // Use utf8 for decoding.
this.encoder = InternalEncoderCesu8
// Add decoder for versions of Node not supporting CESU-8
if (Buffer.from("eda0bdedb2a9", "hex").toString() !== "💩") {
this.decoder = InternalDecoderCesu8
this.defaultCharUnicode = iconv.defaultCharUnicode
}
}
}
InternalCodec.prototype.encoder = InternalEncoder
InternalCodec.prototype.decoder = InternalDecoder
// ------------------------------------------------------------------------------
// We use node.js internal decoder. Its signature is the same as ours.
var StringDecoder = require("string_decoder").StringDecoder
function InternalDecoder (options, codec) {
this.decoder = new StringDecoder(codec.enc)
}
InternalDecoder.prototype.write = function (buf) {
if (!Buffer.isBuffer(buf)) {
buf = Buffer.from(buf)
}
return this.decoder.write(buf)
}
InternalDecoder.prototype.end = function () {
return this.decoder.end()
}
// ------------------------------------------------------------------------------
// Encoder is mostly trivial
function InternalEncoder (options, codec) {
this.enc = codec.enc
}
InternalEncoder.prototype.write = function (str) {
return Buffer.from(str, this.enc)
}
InternalEncoder.prototype.end = function () {
}
// ------------------------------------------------------------------------------
// Except base64 encoder, which must keep its state.
function InternalEncoderBase64 (options, codec) {
this.prevStr = ""
}
InternalEncoderBase64.prototype.write = function (str) {
str = this.prevStr + str
var completeQuads = str.length - (str.length % 4)
this.prevStr = str.slice(completeQuads)
str = str.slice(0, completeQuads)
return Buffer.from(str, "base64")
}
InternalEncoderBase64.prototype.end = function () {
return Buffer.from(this.prevStr, "base64")
}
// ------------------------------------------------------------------------------
// CESU-8 encoder is also special.
function InternalEncoderCesu8 (options, codec) {
}
InternalEncoderCesu8.prototype.write = function (str) {
var buf = Buffer.alloc(str.length * 3); var bufIdx = 0
for (var i = 0; i < str.length; i++) {
var charCode = str.charCodeAt(i)
// Naive implementation, but it works because CESU-8 is especially easy
// to convert from UTF-16 (which all JS strings are encoded in).
if (charCode < 0x80) { buf[bufIdx++] = charCode } else if (charCode < 0x800) {
buf[bufIdx++] = 0xC0 + (charCode >>> 6)
buf[bufIdx++] = 0x80 + (charCode & 0x3f)
} else { // charCode will always be < 0x10000 in javascript.
buf[bufIdx++] = 0xE0 + (charCode >>> 12)
buf[bufIdx++] = 0x80 + ((charCode >>> 6) & 0x3f)
buf[bufIdx++] = 0x80 + (charCode & 0x3f)
}
}
return buf.slice(0, bufIdx)
}
InternalEncoderCesu8.prototype.end = function () {
}
// ------------------------------------------------------------------------------
// CESU-8 decoder is not implemented in Node v4.0+
function InternalDecoderCesu8 (options, codec) {
this.acc = 0
this.contBytes = 0
this.accBytes = 0
this.defaultCharUnicode = codec.defaultCharUnicode
}
InternalDecoderCesu8.prototype.write = function (buf) {
var acc = this.acc; var contBytes = this.contBytes; var accBytes = this.accBytes
var res = ""
for (var i = 0; i < buf.length; i++) {
var curByte = buf[i]
if ((curByte & 0xC0) !== 0x80) { // Leading byte
if (contBytes > 0) { // Previous code is invalid
res += this.defaultCharUnicode
contBytes = 0
}
if (curByte < 0x80) { // Single-byte code
res += String.fromCharCode(curByte)
} else if (curByte < 0xE0) { // Two-byte code
acc = curByte & 0x1F
contBytes = 1; accBytes = 1
} else if (curByte < 0xF0) { // Three-byte code
acc = curByte & 0x0F
contBytes = 2; accBytes = 1
} else { // Four or more are not supported for CESU-8.
res += this.defaultCharUnicode
}
} else { // Continuation byte
if (contBytes > 0) { // We're waiting for it.
acc = (acc << 6) | (curByte & 0x3f)
contBytes--; accBytes++
if (contBytes === 0) {
// Check for overlong encoding, but support Modified UTF-8 (encoding NULL as C0 80)
if (accBytes === 2 && acc < 0x80 && acc > 0) {
res += this.defaultCharUnicode
} else if (accBytes === 3 && acc < 0x800) {
res += this.defaultCharUnicode
} else {
// Actually add character.
res += String.fromCharCode(acc)
}
}
} else { // Unexpected continuation byte
res += this.defaultCharUnicode
}
}
}
this.acc = acc; this.contBytes = contBytes; this.accBytes = accBytes
return res
}
InternalDecoderCesu8.prototype.end = function () {
var res = 0
if (this.contBytes > 0) { res += this.defaultCharUnicode }
return res
}
// ------------------------------------------------------------------------------
// check the chunk boundaries for surrogate pair
function InternalEncoderUtf8 (options, codec) {
this.highSurrogate = ""
}
InternalEncoderUtf8.prototype.write = function (str) {
if (this.highSurrogate) {
str = this.highSurrogate + str
this.highSurrogate = ""
}
if (str.length > 0) {
var charCode = str.charCodeAt(str.length - 1)
if (charCode >= 0xd800 && charCode < 0xdc00) {
this.highSurrogate = str[str.length - 1]
str = str.slice(0, str.length - 1)
}
}
return Buffer.from(str, this.enc)
}
InternalEncoderUtf8.prototype.end = function () {
if (this.highSurrogate) {
var str = this.highSurrogate
this.highSurrogate = ""
return Buffer.from(str, this.enc)
}
}
@@ -0,0 +1,75 @@
"use strict"
var Buffer = require("safer-buffer").Buffer
// Single-byte codec. Needs a 'chars' string parameter that contains 256 or 128 chars that
// correspond to encoded bytes (if 128 - then lower half is ASCII).
exports._sbcs = SBCSCodec
function SBCSCodec (codecOptions, iconv) {
if (!codecOptions) {
throw new Error("SBCS codec is called without the data.")
}
// Prepare char buffer for decoding.
if (!codecOptions.chars || (codecOptions.chars.length !== 128 && codecOptions.chars.length !== 256)) {
throw new Error("Encoding '" + codecOptions.type + "' has incorrect 'chars' (must be of len 128 or 256)")
}
if (codecOptions.chars.length === 128) {
var asciiString = ""
for (var i = 0; i < 128; i++) {
asciiString += String.fromCharCode(i)
}
codecOptions.chars = asciiString + codecOptions.chars
}
this.decodeBuf = Buffer.from(codecOptions.chars, "ucs2")
// Encoding buffer.
var encodeBuf = Buffer.alloc(65536, iconv.defaultCharSingleByte.charCodeAt(0))
for (var i = 0; i < codecOptions.chars.length; i++) {
encodeBuf[codecOptions.chars.charCodeAt(i)] = i
}
this.encodeBuf = encodeBuf
}
SBCSCodec.prototype.encoder = SBCSEncoder
SBCSCodec.prototype.decoder = SBCSDecoder
function SBCSEncoder (options, codec) {
this.encodeBuf = codec.encodeBuf
}
SBCSEncoder.prototype.write = function (str) {
var buf = Buffer.alloc(str.length)
for (var i = 0; i < str.length; i++) {
buf[i] = this.encodeBuf[str.charCodeAt(i)]
}
return buf
}
SBCSEncoder.prototype.end = function () {
}
function SBCSDecoder (options, codec) {
this.decodeBuf = codec.decodeBuf
}
SBCSDecoder.prototype.write = function (buf) {
// Strings are immutable in JS -> we use ucs2 buffer to speed up computations.
var decodeBuf = this.decodeBuf
var newBuf = Buffer.alloc(buf.length * 2)
var idx1 = 0; var idx2 = 0
for (var i = 0; i < buf.length; i++) {
idx1 = buf[i] * 2; idx2 = i * 2
newBuf[idx2] = decodeBuf[idx1]
newBuf[idx2 + 1] = decodeBuf[idx1 + 1]
}
return newBuf.toString("ucs2")
}
SBCSDecoder.prototype.end = function () {
}

Some files were not shown because too many files have changed in this diff Show More