Initial commit
This commit is contained in:
+73
@@ -0,0 +1,73 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// Copyright (c) 2015-2026 MariaDB Corporation Ab
|
||||
|
||||
'use strict';
|
||||
|
||||
// Per-charset multibyte recognizers used by PacketOutputStream#writeBufferEscape
|
||||
// to avoid splitting a valid multibyte character with an inserted escape byte.
|
||||
//
|
||||
// Only charsets whose trail-byte range overlaps the ASCII escape character (0x5C)
|
||||
// are listed here. For them, a naïve byte-wise escape that inserts 0x5C before a
|
||||
// quote/backslash inside arbitrary binary input can produce a wire sequence where
|
||||
// the server lexer eats the inserted 0x5C as the trail byte of a multibyte
|
||||
// character, leaving a bare quote that closes the string literal (SQL injection).
|
||||
//
|
||||
// See for the C reference :
|
||||
// https://github.com/mariadb-corporation/mariadb-connector-c/blob/3.4/libmariadb/ma_charset.c
|
||||
//
|
||||
|
||||
const big5 = {
|
||||
isHead: (b) => b >= 0xa1 && b <= 0xfe,
|
||||
length: (buf, i, n) => {
|
||||
if (i + 1 >= n) return 0;
|
||||
const t = buf[i + 1];
|
||||
return (t >= 0x40 && t <= 0x7e) || (t >= 0xa1 && t <= 0xfe) ? 2 : 0;
|
||||
}
|
||||
};
|
||||
|
||||
const gbk = {
|
||||
isHead: (b) => b >= 0x81 && b <= 0xfe,
|
||||
length: (buf, i, n) => {
|
||||
if (i + 1 >= n) return 0;
|
||||
const t = buf[i + 1];
|
||||
return (t >= 0x40 && t <= 0x7e) || (t >= 0x80 && t <= 0xfe) ? 2 : 0;
|
||||
}
|
||||
};
|
||||
|
||||
const sjis = {
|
||||
isHead: (b) => (b >= 0x81 && b <= 0x9f) || (b >= 0xe0 && b <= 0xfc),
|
||||
length: (buf, i, n) => {
|
||||
if (i + 1 >= n) return 0;
|
||||
const t = buf[i + 1];
|
||||
return (t >= 0x40 && t <= 0x7e) || (t >= 0x80 && t <= 0xfc) ? 2 : 0;
|
||||
}
|
||||
};
|
||||
|
||||
// cp932 shares sjis ranges
|
||||
const cp932 = sjis;
|
||||
|
||||
const gbOdd = (b) => b >= 0x81 && b <= 0xfe;
|
||||
const gbEven2 = (b) => (b >= 0x40 && b <= 0x7e) || (b >= 0x80 && b <= 0xfe);
|
||||
const gbEven4 = (b) => b >= 0x30 && b <= 0x39;
|
||||
|
||||
const gb18030 = {
|
||||
isHead: gbOdd,
|
||||
length: (buf, i, n) => {
|
||||
if (i + 1 >= n) return 0;
|
||||
if (gbEven2(buf[i + 1])) return 2;
|
||||
if (i + 3 < n && gbEven4(buf[i + 1]) && gbOdd(buf[i + 2]) && gbEven4(buf[i + 3])) {
|
||||
return 4;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
const recognizers = {
|
||||
big5: big5,
|
||||
gbk: gbk,
|
||||
sjis: sjis,
|
||||
cp932: cp932,
|
||||
gb18030: gb18030
|
||||
};
|
||||
|
||||
export const getMbRecognizer = (encoding) => recognizers[encoding] || null;
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// Copyright (c) 2015-2024 MariaDB Corporation Ab
|
||||
|
||||
'use strict';
|
||||
|
||||
class ConnectionInformation {
|
||||
#redirectFct;
|
||||
constructor(opts, redirectFct) {
|
||||
this.threadId = -1;
|
||||
this.status = null;
|
||||
this.serverVersion = null;
|
||||
this.serverCapabilities = null;
|
||||
this.database = opts.database;
|
||||
this.port = opts.port;
|
||||
this.#redirectFct = redirectFct;
|
||||
this.redirectRequest = null;
|
||||
}
|
||||
|
||||
hasMinVersion(major, minor, patch) {
|
||||
if (!this.serverVersion) throw new Error('cannot know if server version until connection is established');
|
||||
|
||||
if (!major) throw new Error('a major version must be set');
|
||||
|
||||
if (!minor) minor = 0;
|
||||
if (!patch) patch = 0;
|
||||
|
||||
let ver = this.serverVersion;
|
||||
return (
|
||||
ver.major > major ||
|
||||
(ver.major === major && ver.minor > minor) ||
|
||||
(ver.major === major && ver.minor === minor && ver.patch >= patch)
|
||||
);
|
||||
}
|
||||
|
||||
redirect(value, resolve) {
|
||||
return this.#redirectFct(value, resolve);
|
||||
}
|
||||
|
||||
isMariaDB() {
|
||||
if (!this.serverVersion) throw new Error('cannot know if server is MariaDB until connection is established');
|
||||
return this.serverVersion.mariaDb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse raw info to set server major/minor/patch values
|
||||
* @param info
|
||||
*/
|
||||
static parseVersionString(info) {
|
||||
let car;
|
||||
let offset = 0;
|
||||
let type = 0;
|
||||
let val = 0;
|
||||
|
||||
for (; offset < info.serverVersion.raw.length; offset++) {
|
||||
car = info.serverVersion.raw.charCodeAt(offset);
|
||||
if (car < 48 || car > 57) {
|
||||
switch (type) {
|
||||
case 0:
|
||||
info.serverVersion.major = val;
|
||||
break;
|
||||
case 1:
|
||||
info.serverVersion.minor = val;
|
||||
break;
|
||||
case 2:
|
||||
info.serverVersion.patch = val;
|
||||
return;
|
||||
}
|
||||
type++;
|
||||
val = 0;
|
||||
} else {
|
||||
val = val * 10 + car - 48;
|
||||
}
|
||||
}
|
||||
//serverVersion finished by number like "5.5.57", assign patchVersion
|
||||
if (type === 2) info.serverVersion.patch = val;
|
||||
}
|
||||
}
|
||||
|
||||
export default ConnectionInformation;
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// Copyright (c) 2015-2025 MariaDB Corporation Ab
|
||||
|
||||
'use strict';
|
||||
import ErrorCodes from '../const/error-code.js';
|
||||
|
||||
export default class SqlError extends Error {
|
||||
constructor(msg, sql, fatal, info, sqlState, errno, additionalStack, addHeader = undefined, cause) {
|
||||
super(
|
||||
(addHeader !== false
|
||||
? `(conn:${info && info.threadId ? info.threadId : -1}, no: ${errno ? errno : -1}, SQLState: ${sqlState}) `
|
||||
: '') +
|
||||
msg +
|
||||
(sql ? '\nsql: ' + sql : ''),
|
||||
cause
|
||||
);
|
||||
this.name = 'SqlError';
|
||||
this.sqlMessage = msg;
|
||||
this.sql = sql;
|
||||
this.fatal = fatal;
|
||||
this.errno = errno;
|
||||
this.sqlState = sqlState;
|
||||
if (errno > 45000 && errno < 46000) {
|
||||
//driver error
|
||||
this.code = getClientErrorKey(errno) || 'UNKNOWN';
|
||||
} else {
|
||||
this.code = ErrorCodes[this.errno] || 'UNKNOWN';
|
||||
}
|
||||
if (additionalStack) {
|
||||
//adding caller stack, removing initial "Error:\n"
|
||||
this.stack += '\n From event:\n' + additionalStack.substring(additionalStack.indexOf('\n') + 1);
|
||||
}
|
||||
}
|
||||
|
||||
get text() {
|
||||
return this.sqlMessage;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Error factory, so error gets connection information.
|
||||
*
|
||||
* @param msg current error message
|
||||
* @param errno error number
|
||||
* @param info connection information
|
||||
* @param sqlState sql state
|
||||
* @param sql sql command
|
||||
* @param fatal is error fatal
|
||||
* @param additionalStack additional stack trace to see
|
||||
* @param addHeader add connection information
|
||||
* @param cause add cause
|
||||
* @returns {Error} the error
|
||||
*/
|
||||
export function createError(
|
||||
msg,
|
||||
errno,
|
||||
info = null,
|
||||
sqlState = 'HY000',
|
||||
sql = null,
|
||||
fatal = false,
|
||||
additionalStack = undefined,
|
||||
addHeader = undefined,
|
||||
cause = undefined
|
||||
) {
|
||||
if (cause) return new SqlError(msg, sql, fatal, info, sqlState, errno, additionalStack, addHeader, { cause: cause });
|
||||
return new SqlError(msg, sql, fatal, info, sqlState, errno, additionalStack, addHeader, cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fatal error factory, so error gets connection information.
|
||||
*
|
||||
* @param msg current error message
|
||||
* @param errno error number
|
||||
* @param info connection information
|
||||
* @param sqlState sql state
|
||||
* @param sql sql command
|
||||
* @param additionalStack additional stack trace to see
|
||||
* @param addHeader add connection information
|
||||
* @returns {Error} the error
|
||||
*/
|
||||
export function createFatalError(
|
||||
msg,
|
||||
errno,
|
||||
info = null,
|
||||
sqlState = '08S01',
|
||||
sql = null,
|
||||
additionalStack = undefined,
|
||||
addHeader = undefined
|
||||
) {
|
||||
return new SqlError(msg, sql, true, info, sqlState, errno, additionalStack, addHeader);
|
||||
}
|
||||
|
||||
/********************************************************************************
|
||||
* Driver-specific errors
|
||||
********************************************************************************/
|
||||
|
||||
export const client = {
|
||||
ER_CONNECTION_ALREADY_CLOSED: 45001,
|
||||
ER_MYSQL_CHANGE_USER_BUG: 45003,
|
||||
ER_CMD_NOT_EXECUTED_DESTROYED: 45004,
|
||||
ER_NULL_CHAR_ESCAPEID: 45005,
|
||||
ER_NULL_ESCAPEID: 45006,
|
||||
ER_NOT_IMPLEMENTED_FORMAT: 45007,
|
||||
ER_NODE_NOT_SUPPORTED_TLS: 45008,
|
||||
ER_SOCKET_UNEXPECTED_CLOSE: 45009,
|
||||
ER_UNEXPECTED_PACKET: 45011,
|
||||
ER_CONNECTION_TIMEOUT: 45012,
|
||||
ER_CMD_CONNECTION_CLOSED: 45013,
|
||||
ER_CHANGE_USER_BAD_PACKET: 45014,
|
||||
ER_PING_BAD_PACKET: 45015,
|
||||
ER_MISSING_PARAMETER: 45016,
|
||||
ER_PARAMETER_UNDEFINED: 45017,
|
||||
ER_PLACEHOLDER_UNDEFINED: 45018,
|
||||
ER_SOCKET: 45019,
|
||||
ER_EOF_EXPECTED: 45020,
|
||||
ER_LOCAL_INFILE_DISABLED: 45021,
|
||||
ER_LOCAL_INFILE_NOT_READABLE: 45022,
|
||||
ER_SERVER_SSL_DISABLED: 45023,
|
||||
ER_AUTHENTICATION_BAD_PACKET: 45024,
|
||||
ER_AUTHENTICATION_PLUGIN_NOT_SUPPORTED: 45025,
|
||||
ER_SOCKET_TIMEOUT: 45026,
|
||||
ER_POOL_ALREADY_CLOSED: 45027,
|
||||
ER_GET_CONNECTION_TIMEOUT: 45028,
|
||||
ER_SETTING_SESSION_ERROR: 45029,
|
||||
ER_INITIAL_SQL_ERROR: 45030,
|
||||
ER_BATCH_WITH_NO_VALUES: 45031,
|
||||
ER_RESET_BAD_PACKET: 45032,
|
||||
ER_WRONG_IANA_TIMEZONE: 45033,
|
||||
ER_LOCAL_INFILE_WRONG_FILENAME: 45034,
|
||||
ER_ADD_CONNECTION_CLOSED_POOL: 45035,
|
||||
ER_WRONG_AUTO_TIMEZONE: 45036,
|
||||
ER_CLOSING_POOL: 45037,
|
||||
ER_TIMEOUT_NOT_SUPPORTED: 45038,
|
||||
ER_INITIAL_TIMEOUT_ERROR: 45039,
|
||||
ER_DUPLICATE_FIELD: 45040,
|
||||
ER_PING_TIMEOUT: 45042,
|
||||
ER_BAD_PARAMETER_VALUE: 45043,
|
||||
ER_CANNOT_RETRIEVE_RSA_KEY: 45044,
|
||||
ER_MINIMUM_NODE_VERSION_REQUIRED: 45045,
|
||||
ER_MAX_ALLOWED_PACKET: 45046,
|
||||
ER_NOT_SUPPORTED_AUTH_PLUGIN: 45047,
|
||||
ER_COMPRESSION_NOT_SUPPORTED: 45048,
|
||||
ER_UNDEFINED_SQL: 45049,
|
||||
ER_PARSING_PRECISION: 45050,
|
||||
ER_PREPARE_CLOSED: 45051,
|
||||
ER_MISSING_SQL_PARAMETER: 45052,
|
||||
ER_MISSING_SQL_FILE: 45053,
|
||||
ER_SQL_FILE_ERROR: 45054,
|
||||
ER_MISSING_DATABASE_PARAMETER: 45055,
|
||||
ER_SELF_SIGNED: 45056,
|
||||
ER_SELF_SIGNED_NO_PWD: 45057,
|
||||
ER_PRIVATE_FIELDS_USE: 45058,
|
||||
ER_TLS_IDENTITY_ERROR: 45059,
|
||||
ER_POOL_NOT_INITIALIZED: 45060,
|
||||
ER_POOL_NO_CONNECTION: 45061,
|
||||
ER_SELF_SIGNED_BAD_PLUGIN: 45062,
|
||||
ER_SELF_SIGNED_SHA256: 45063,
|
||||
ER_CLEAR_PASSWORD_WITHOUT_SSL: 45064
|
||||
};
|
||||
|
||||
export function getClientErrorKey(errno) {
|
||||
for (const [key, value] of Object.entries(client)) {
|
||||
if (value === errno) return key;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
+542
@@ -0,0 +1,542 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// Copyright (c) 2015-2025 MariaDB Corporation Ab
|
||||
|
||||
import * as Errors from '../misc/errors.js';
|
||||
|
||||
const State = {
|
||||
Normal: 1 /* query */,
|
||||
String: 2 /* inside string */,
|
||||
SlashStarComment: 3 /* inside slash-star comment */,
|
||||
Escape: 4 /* found backslash */,
|
||||
EOLComment: 5 /* # comment, or // comment, or -- comment */,
|
||||
Backtick: 6 /* found backtick */,
|
||||
Placeholder: 7 /* found placeholder */
|
||||
};
|
||||
|
||||
const SLASH_BYTE = '/'.charCodeAt(0);
|
||||
const STAR_BYTE = '*'.charCodeAt(0);
|
||||
const BACKSLASH_BYTE = '\\'.charCodeAt(0);
|
||||
const HASH_BYTE = '#'.charCodeAt(0);
|
||||
const MINUS_BYTE = '-'.charCodeAt(0);
|
||||
const LINE_FEED_BYTE = '\n'.charCodeAt(0);
|
||||
const DBL_QUOTE_BYTE = '"'.charCodeAt(0);
|
||||
const QUOTE_BYTE = "'".charCodeAt(0);
|
||||
const RADICAL_BYTE = '`'.charCodeAt(0);
|
||||
const QUESTION_MARK_BYTE = '?'.charCodeAt(0);
|
||||
const COLON_BYTE = ':'.charCodeAt(0);
|
||||
const SEMICOLON_BYTE = ';'.charCodeAt(0);
|
||||
|
||||
/**
|
||||
* Search for question mark positions.
|
||||
* Question marks in comment are not taken into account
|
||||
*
|
||||
* @returns {Array} question mark position
|
||||
*/
|
||||
export function splitQuery(query) {
|
||||
let paramPositions = [];
|
||||
let state = State.Normal;
|
||||
let lastChar = 0x00;
|
||||
let singleQuotes = false;
|
||||
let currentChar;
|
||||
|
||||
const len = query.length;
|
||||
for (let i = 0; i < len; i++) {
|
||||
currentChar = query[i];
|
||||
if (
|
||||
state === State.Escape &&
|
||||
!((currentChar === QUOTE_BYTE && singleQuotes) || (currentChar === DBL_QUOTE_BYTE && !singleQuotes))
|
||||
) {
|
||||
state = State.String;
|
||||
lastChar = currentChar;
|
||||
continue;
|
||||
}
|
||||
switch (currentChar) {
|
||||
case STAR_BYTE:
|
||||
if (state === State.Normal && lastChar === SLASH_BYTE) {
|
||||
state = State.SlashStarComment;
|
||||
}
|
||||
break;
|
||||
|
||||
case SLASH_BYTE:
|
||||
if (state === State.SlashStarComment && lastChar === STAR_BYTE) {
|
||||
state = State.Normal;
|
||||
lastChar = 0;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
|
||||
case HASH_BYTE:
|
||||
if (state === State.Normal) {
|
||||
state = State.EOLComment;
|
||||
}
|
||||
break;
|
||||
|
||||
case MINUS_BYTE:
|
||||
if (state === State.Normal && lastChar === MINUS_BYTE) {
|
||||
const next = i + 1 < len ? query[i + 1] : 0;
|
||||
if (next <= 0x20) state = State.EOLComment;
|
||||
}
|
||||
break;
|
||||
|
||||
case LINE_FEED_BYTE:
|
||||
if (state === State.EOLComment) {
|
||||
state = State.Normal;
|
||||
}
|
||||
break;
|
||||
|
||||
case DBL_QUOTE_BYTE:
|
||||
if (state === State.Normal) {
|
||||
state = State.String;
|
||||
singleQuotes = false;
|
||||
} else if (state === State.String && !singleQuotes) {
|
||||
state = State.Normal;
|
||||
} else if (state === State.Escape) {
|
||||
state = State.String;
|
||||
}
|
||||
break;
|
||||
|
||||
case QUOTE_BYTE:
|
||||
if (state === State.Normal) {
|
||||
state = State.String;
|
||||
singleQuotes = true;
|
||||
} else if (state === State.String && singleQuotes) {
|
||||
state = State.Normal;
|
||||
} else if (state === State.Escape) {
|
||||
state = State.String;
|
||||
}
|
||||
break;
|
||||
|
||||
case BACKSLASH_BYTE:
|
||||
if (state === State.String) {
|
||||
state = State.Escape;
|
||||
}
|
||||
break;
|
||||
case QUESTION_MARK_BYTE:
|
||||
if (state === State.Normal) {
|
||||
paramPositions.push(i, ++i);
|
||||
}
|
||||
break;
|
||||
case RADICAL_BYTE:
|
||||
if (state === State.Backtick) {
|
||||
state = State.Normal;
|
||||
} else if (state === State.Normal) {
|
||||
state = State.Backtick;
|
||||
}
|
||||
break;
|
||||
}
|
||||
lastChar = currentChar;
|
||||
}
|
||||
return paramPositions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Split the query according to parameters using placeholder.
|
||||
*
|
||||
* @param query query bytes
|
||||
* @param info connection information
|
||||
* @param initialValues placeholder object
|
||||
* @param displaySql display sql function
|
||||
* @returns {{paramPositions: Array, values: Array}}
|
||||
*/
|
||||
export function splitQueryPlaceholder(query, info, initialValues, displaySql) {
|
||||
let placeholderValues = Object.assign({}, initialValues);
|
||||
let paramPositions = [];
|
||||
let values = [];
|
||||
let state = State.Normal;
|
||||
let lastChar = 0x00;
|
||||
let singleQuotes = false;
|
||||
let car;
|
||||
|
||||
const len = query.length;
|
||||
for (let i = 0; i < len; i++) {
|
||||
car = query[i];
|
||||
if (
|
||||
state === State.Escape &&
|
||||
!((car === QUOTE_BYTE && singleQuotes) || (car === DBL_QUOTE_BYTE && !singleQuotes))
|
||||
) {
|
||||
state = State.String;
|
||||
lastChar = car;
|
||||
continue;
|
||||
}
|
||||
switch (car) {
|
||||
case STAR_BYTE:
|
||||
if (state === State.Normal && lastChar === SLASH_BYTE) {
|
||||
state = State.SlashStarComment;
|
||||
}
|
||||
break;
|
||||
|
||||
case SLASH_BYTE:
|
||||
if (state === State.SlashStarComment && lastChar === STAR_BYTE) {
|
||||
state = State.Normal;
|
||||
lastChar = 0;
|
||||
continue;
|
||||
} else if (state === State.Normal && lastChar === SLASH_BYTE) {
|
||||
state = State.EOLComment;
|
||||
}
|
||||
break;
|
||||
|
||||
case HASH_BYTE:
|
||||
if (state === State.Normal) {
|
||||
state = State.EOLComment;
|
||||
}
|
||||
break;
|
||||
|
||||
case MINUS_BYTE:
|
||||
if (state === State.Normal && lastChar === MINUS_BYTE) {
|
||||
const next = i + 1 < len ? query[i + 1] : 0;
|
||||
if (next <= 0x20) state = State.EOLComment;
|
||||
}
|
||||
break;
|
||||
|
||||
case LINE_FEED_BYTE:
|
||||
if (state === State.EOLComment) {
|
||||
state = State.Normal;
|
||||
}
|
||||
break;
|
||||
|
||||
case DBL_QUOTE_BYTE:
|
||||
if (state === State.Normal) {
|
||||
state = State.String;
|
||||
singleQuotes = false;
|
||||
} else if (state === State.String && !singleQuotes) {
|
||||
state = State.Normal;
|
||||
} else if (state === State.Escape) {
|
||||
state = State.String;
|
||||
}
|
||||
break;
|
||||
|
||||
case QUOTE_BYTE:
|
||||
if (state === State.Normal) {
|
||||
state = State.String;
|
||||
singleQuotes = true;
|
||||
} else if (state === State.String && singleQuotes) {
|
||||
state = State.Normal;
|
||||
} else if (state === State.Escape) {
|
||||
state = State.String;
|
||||
}
|
||||
break;
|
||||
|
||||
case BACKSLASH_BYTE:
|
||||
if (state === State.String) {
|
||||
state = State.Escape;
|
||||
}
|
||||
break;
|
||||
case QUESTION_MARK_BYTE:
|
||||
if (state === State.Normal) {
|
||||
const key = Object.keys(placeholderValues)[0];
|
||||
values.push(placeholderValues[key]);
|
||||
delete placeholderValues[key];
|
||||
|
||||
paramPositions.push(i);
|
||||
paramPositions.push(++i);
|
||||
}
|
||||
break;
|
||||
case COLON_BYTE:
|
||||
if (state === State.Normal) {
|
||||
let j = 1;
|
||||
|
||||
while (
|
||||
(i + j < len && query[i + j] >= '0'.charCodeAt(0) && query[i + j] <= '9'.charCodeAt(0)) ||
|
||||
(query[i + j] >= 'A'.charCodeAt(0) && query[i + j] <= 'Z'.charCodeAt(0)) ||
|
||||
(query[i + j] >= 'a'.charCodeAt(0) && query[i + j] <= 'z'.charCodeAt(0)) ||
|
||||
query[i + j] === '-'.charCodeAt(0) ||
|
||||
query[i + j] === '_'.charCodeAt(0)
|
||||
) {
|
||||
j++;
|
||||
}
|
||||
|
||||
paramPositions.push(i, i + j);
|
||||
|
||||
const placeholderName = query.toString('utf8', i + 1, i + j);
|
||||
i += j;
|
||||
let val;
|
||||
if (placeholderName in placeholderValues) {
|
||||
val = placeholderValues[placeholderName];
|
||||
delete placeholderValues[placeholderName];
|
||||
} else {
|
||||
// value is already used
|
||||
val = initialValues[placeholderName];
|
||||
}
|
||||
|
||||
if (val === undefined) {
|
||||
throw Errors.createError(
|
||||
`Placeholder '${placeholderName}' is not defined`,
|
||||
Errors.client.ER_PLACEHOLDER_UNDEFINED,
|
||||
info,
|
||||
'HY000',
|
||||
displaySql.call()
|
||||
);
|
||||
}
|
||||
values.push(val);
|
||||
}
|
||||
break;
|
||||
case RADICAL_BYTE:
|
||||
if (state === State.Backtick) {
|
||||
state = State.Normal;
|
||||
} else if (state === State.Normal) {
|
||||
state = State.Backtick;
|
||||
}
|
||||
break;
|
||||
}
|
||||
lastChar = car;
|
||||
}
|
||||
return { paramPositions: paramPositions, values: values };
|
||||
}
|
||||
|
||||
export function searchPlaceholder(sql) {
|
||||
let sqlPlaceHolder = '';
|
||||
let placeHolderIndex = [];
|
||||
let state = State.Normal;
|
||||
let lastChar = '\0';
|
||||
|
||||
let singleQuotes = false;
|
||||
let lastParameterPosition = 0;
|
||||
|
||||
let idx = 0;
|
||||
let car = sql.charAt(idx++);
|
||||
let placeholderName;
|
||||
|
||||
while (car !== '') {
|
||||
if (state === State.Escape && !((car === "'" && singleQuotes) || (car === '"' && !singleQuotes))) {
|
||||
state = State.String;
|
||||
lastChar = car;
|
||||
car = sql.charAt(idx++);
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (car) {
|
||||
case '*':
|
||||
if (state === State.Normal && lastChar === '/') state = State.SlashStarComment;
|
||||
break;
|
||||
|
||||
case '/':
|
||||
if (state === State.SlashStarComment && lastChar === '*') {
|
||||
state = State.Normal;
|
||||
lastChar = '\0';
|
||||
car = sql.charAt(idx++);
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
|
||||
case '#':
|
||||
if (state === State.Normal) state = State.EOLComment;
|
||||
break;
|
||||
|
||||
case '-':
|
||||
if (state === State.Normal && lastChar === '-') {
|
||||
const next = sql.charAt(idx);
|
||||
if (next === '' || next <= ' ') state = State.EOLComment;
|
||||
}
|
||||
break;
|
||||
|
||||
case '\n':
|
||||
if (state === State.EOLComment) {
|
||||
state = State.Normal;
|
||||
}
|
||||
break;
|
||||
|
||||
case '"':
|
||||
if (state === State.Normal) {
|
||||
state = State.String;
|
||||
singleQuotes = false;
|
||||
} else if (state === State.String && !singleQuotes) {
|
||||
state = State.Normal;
|
||||
} else if (state === State.Escape && !singleQuotes) {
|
||||
state = State.String;
|
||||
}
|
||||
break;
|
||||
|
||||
case "'":
|
||||
if (state === State.Normal) {
|
||||
state = State.String;
|
||||
singleQuotes = true;
|
||||
} else if (state === State.String && singleQuotes) {
|
||||
state = State.Normal;
|
||||
singleQuotes = false;
|
||||
} else if (state === State.Escape && singleQuotes) {
|
||||
state = State.String;
|
||||
}
|
||||
break;
|
||||
|
||||
case '\\':
|
||||
if (state === State.String) state = State.Escape;
|
||||
break;
|
||||
|
||||
case ':':
|
||||
if (state === State.Normal) {
|
||||
sqlPlaceHolder += sql.substring(lastParameterPosition, idx - 1) + '?';
|
||||
placeholderName = '';
|
||||
while (
|
||||
((car = sql.charAt(idx++)) !== '' && car >= '0' && car <= '9') ||
|
||||
(car >= 'A' && car <= 'Z') ||
|
||||
(car >= 'a' && car <= 'z') ||
|
||||
car === '-' ||
|
||||
car === '_'
|
||||
) {
|
||||
placeholderName += car;
|
||||
}
|
||||
idx--;
|
||||
placeHolderIndex.push(placeholderName);
|
||||
lastParameterPosition = idx;
|
||||
}
|
||||
break;
|
||||
case '`':
|
||||
if (state === State.Backtick) {
|
||||
state = State.Normal;
|
||||
} else if (state === State.Normal) {
|
||||
state = State.Backtick;
|
||||
}
|
||||
}
|
||||
lastChar = car;
|
||||
|
||||
car = sql.charAt(idx++);
|
||||
}
|
||||
if (lastParameterPosition === 0) {
|
||||
sqlPlaceHolder = sql;
|
||||
} else {
|
||||
sqlPlaceHolder += sql.substring(lastParameterPosition);
|
||||
}
|
||||
|
||||
return { sql: sqlPlaceHolder, placeHolderIndex: placeHolderIndex };
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that filename requested by server corresponds to query
|
||||
* protocol : https://mariadb.com/kb/en/library/local_infile-packet/
|
||||
*
|
||||
* @param sql query
|
||||
* @param parameters parameters if any
|
||||
* @param fileName server requested file
|
||||
* @returns {boolean} is filename corresponding to query
|
||||
*/
|
||||
export function validateFileName(sql, parameters, fileName) {
|
||||
// in the case of windows, the file name in a query is escaped
|
||||
// so for example, LOAD DATA LOCAL INFILE 'C:\\Temp\\myFile.txt' ...
|
||||
// but the server returns 'C:\Temp\myFile.txt'
|
||||
// so with regex escaped, must test LOAD DATA LOCAL INFILE 'C:\\\\Temp\\\\myFile.txt'
|
||||
let queryValidator = new RegExp(
|
||||
"^(\\s*\\/\\*([^\\*]|\\*[^\\/])*\\*\\/)*\\s*LOAD\\s+DATA\\s+((LOW_PRIORITY|CONCURRENT)\\s+)?LOCAL\\s+INFILE\\s+'" +
|
||||
fileName.replace(/\\/g, '\\\\\\\\').replace('.', '\\.') +
|
||||
"'",
|
||||
'i'
|
||||
);
|
||||
if (queryValidator.test(sql)) return true;
|
||||
|
||||
if (parameters != null) {
|
||||
queryValidator = new RegExp(
|
||||
'^(\\s*\\/\\*([^\\*]|\\*[^\\/])*\\*\\/)*\\s*LOAD\\s+DATA\\s+((LOW_PRIORITY|CONCURRENT)\\s+)?' +
|
||||
'LOCAL\\s+INFILE\\s+\\?',
|
||||
'i'
|
||||
);
|
||||
if (queryValidator.test(sql) && parameters.length > 0) {
|
||||
if (Array.isArray(parameters)) {
|
||||
return parameters[0].toLowerCase() === fileName.toLowerCase();
|
||||
}
|
||||
return parameters.toLowerCase() === fileName.toLowerCase();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse commands from buffer, returns queries separated by ';'
|
||||
* (the last one is not parsed)
|
||||
*
|
||||
* @param bufState buffer
|
||||
* @returns {*[]} array of queries contained in buffer
|
||||
*/
|
||||
export function parseQueries(bufState) {
|
||||
let state = State.Normal;
|
||||
let lastChar = 0x00;
|
||||
let currByte;
|
||||
let queries = [];
|
||||
let singleQuotes = false;
|
||||
|
||||
for (let i = bufState.offset; i < bufState.end; i++) {
|
||||
currByte = bufState.buffer[i];
|
||||
if (
|
||||
state === State.Escape &&
|
||||
!((currByte === QUOTE_BYTE && singleQuotes) || (currByte === DBL_QUOTE_BYTE && !singleQuotes))
|
||||
) {
|
||||
state = State.String;
|
||||
lastChar = currByte;
|
||||
continue;
|
||||
}
|
||||
switch (currByte) {
|
||||
case STAR_BYTE:
|
||||
if (state === State.Normal && lastChar === SLASH_BYTE) {
|
||||
state = State.SlashStarComment;
|
||||
}
|
||||
break;
|
||||
|
||||
case SLASH_BYTE:
|
||||
if (state === State.SlashStarComment && lastChar === STAR_BYTE) {
|
||||
state = State.Normal;
|
||||
} else if (state === State.Normal && lastChar === SLASH_BYTE) {
|
||||
state = State.EOLComment;
|
||||
}
|
||||
break;
|
||||
|
||||
case HASH_BYTE:
|
||||
if (state === State.Normal) {
|
||||
state = State.EOLComment;
|
||||
}
|
||||
break;
|
||||
|
||||
case MINUS_BYTE:
|
||||
if (state === State.Normal && lastChar === MINUS_BYTE) {
|
||||
state = State.EOLComment;
|
||||
}
|
||||
break;
|
||||
|
||||
case LINE_FEED_BYTE:
|
||||
if (state === State.EOLComment) {
|
||||
state = State.Normal;
|
||||
}
|
||||
break;
|
||||
|
||||
case DBL_QUOTE_BYTE:
|
||||
if (state === State.Normal) {
|
||||
state = State.String;
|
||||
singleQuotes = false;
|
||||
} else if (state === State.String && !singleQuotes) {
|
||||
state = State.Normal;
|
||||
} else if (state === State.Escape) {
|
||||
state = State.String;
|
||||
}
|
||||
break;
|
||||
|
||||
case QUOTE_BYTE:
|
||||
if (state === State.Normal) {
|
||||
state = State.String;
|
||||
singleQuotes = true;
|
||||
} else if (state === State.String && singleQuotes) {
|
||||
state = State.Normal;
|
||||
} else if (state === State.Escape) {
|
||||
state = State.String;
|
||||
}
|
||||
break;
|
||||
|
||||
case BACKSLASH_BYTE:
|
||||
if (state === State.String) {
|
||||
state = State.Escape;
|
||||
}
|
||||
break;
|
||||
case SEMICOLON_BYTE:
|
||||
if (state === State.Normal) {
|
||||
queries.push(bufState.buffer.toString('utf8', bufState.offset, i));
|
||||
bufState.offset = i + 1;
|
||||
}
|
||||
break;
|
||||
case RADICAL_BYTE:
|
||||
if (state === State.Backtick) {
|
||||
state = State.Normal;
|
||||
} else if (state === State.Normal) {
|
||||
state = State.Backtick;
|
||||
}
|
||||
break;
|
||||
}
|
||||
lastChar = currByte;
|
||||
}
|
||||
return queries;
|
||||
}
|
||||
+270
@@ -0,0 +1,270 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// Copyright (c) 2015-2024 MariaDB Corporation Ab
|
||||
|
||||
'use strict';
|
||||
import * as Errors from '../misc/errors.js';
|
||||
import Iconv from 'iconv-lite';
|
||||
import TextEncoder from '../cmd/encoder/text-encoder.js';
|
||||
|
||||
const hexArray = '0123456789ABCDEF'.split('');
|
||||
|
||||
/**
|
||||
* Write bytes/hexadecimal value of a byte array to a string.
|
||||
* String output example:
|
||||
* 38 00 00 00 03 63 72 65 61 74 65 20 74 61 62 6C 8....create tabl
|
||||
* 65 20 42 6C 6F 62 54 65 73 74 63 6C 6F 62 74 65 e BlobTestclobte
|
||||
* 73 74 32 20 28 73 74 72 6D 20 74 65 78 74 29 20 st2 (strm text)
|
||||
* 43 48 41 52 53 45 54 20 75 74 66 38 CHARSET utf8
|
||||
*/
|
||||
function log(opts, buf, off, end, header) {
|
||||
let out = [];
|
||||
if (!buf) return '';
|
||||
if (off === undefined || off === null) off = 0;
|
||||
if (end === undefined || end === null) end = buf.length;
|
||||
let asciiValue = new Array(16);
|
||||
asciiValue[8] = ' ';
|
||||
|
||||
let useHeader = header !== undefined;
|
||||
let offset = off || 0;
|
||||
const maxLgh = Math.min(useHeader ? opts.debugLen - header.length : opts.debugLen, end - offset);
|
||||
const isLimited = end - offset > maxLgh;
|
||||
let byteValue;
|
||||
let posHexa = 0;
|
||||
let pos = 0;
|
||||
|
||||
out.push(
|
||||
'+--------------------------------------------------+\n' +
|
||||
'| 0 1 2 3 4 5 6 7 8 9 a b c d e f |\n' +
|
||||
'+--------------------------------------------------+------------------+\n'
|
||||
);
|
||||
|
||||
if (useHeader) {
|
||||
while (pos < header.length) {
|
||||
if (posHexa === 0) out.push('| ');
|
||||
byteValue = header[pos++] & 0xff;
|
||||
out.push(hexArray[byteValue >>> 4], hexArray[byteValue & 0x0f], ' ');
|
||||
asciiValue[posHexa++] = byteValue > 31 && byteValue < 127 ? String.fromCharCode(byteValue) : '.';
|
||||
if (posHexa === 8) out.push(' ');
|
||||
}
|
||||
}
|
||||
|
||||
pos = offset;
|
||||
while (pos < maxLgh + offset) {
|
||||
if (posHexa === 0) out.push('| ');
|
||||
byteValue = buf[pos] & 0xff;
|
||||
|
||||
out.push(hexArray[byteValue >>> 4], hexArray[byteValue & 0x0f], ' ');
|
||||
|
||||
asciiValue[posHexa++] = byteValue > 31 && byteValue < 127 ? String.fromCharCode(byteValue) : '.';
|
||||
|
||||
if (posHexa === 8) out.push(' ');
|
||||
if (posHexa === 16) {
|
||||
out.push('| ', asciiValue.join(''), ' |\n');
|
||||
posHexa = 0;
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
|
||||
let remaining = posHexa;
|
||||
if (remaining > 0) {
|
||||
if (remaining < 8) {
|
||||
for (; remaining < 8; remaining++) {
|
||||
out.push(' ');
|
||||
asciiValue[posHexa++] = ' ';
|
||||
}
|
||||
out.push(' ');
|
||||
}
|
||||
|
||||
for (; remaining < 16; remaining++) {
|
||||
out.push(' ');
|
||||
asciiValue[posHexa++] = ' ';
|
||||
}
|
||||
|
||||
out.push('| ', asciiValue.join(''), isLimited ? ' |...\n' : ' |\n');
|
||||
} else if (isLimited) {
|
||||
out[out.length - 1] = ' |...\n';
|
||||
}
|
||||
out.push('+--------------------------------------------------+------------------+\n');
|
||||
return out.join('');
|
||||
}
|
||||
|
||||
function toHexString(bytes) {
|
||||
return Array.from(bytes, (byte) => {
|
||||
return ('0' + (byte & 0xff).toString(16)).slice(-2);
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function escapeId(opts, info, value) {
|
||||
if (!value || value === '') {
|
||||
throw Errors.createError('Cannot escape empty ID value', Errors.client.ER_NULL_ESCAPEID, info, '0A000');
|
||||
}
|
||||
if (value.includes('\u0000')) {
|
||||
throw Errors.createError(
|
||||
'Cannot escape ID with null character (u0000)',
|
||||
Errors.client.ER_NULL_CHAR_ESCAPEID,
|
||||
info,
|
||||
'0A000'
|
||||
);
|
||||
}
|
||||
// always return escaped value, even when there are no special characters
|
||||
// to permit working with reserved words
|
||||
return '`' + value.replace(/`/g, '``') + '`';
|
||||
}
|
||||
|
||||
function escapeParameters(opts, info, value) {
|
||||
if (value == null) return 'NULL';
|
||||
|
||||
switch (typeof value) {
|
||||
case 'boolean':
|
||||
return value ? 'true' : 'false';
|
||||
case 'bigint':
|
||||
case 'number':
|
||||
return `${value}`;
|
||||
case 'object':
|
||||
if (Object.prototype.toString.call(value) === '[object Date]') {
|
||||
return TextEncoder.getFixedFormatDate(value);
|
||||
} else if (Buffer.isBuffer(value)) {
|
||||
let stValue;
|
||||
if (Buffer.isEncoding(info.collation.charset)) {
|
||||
stValue = value.toString(info.collation.charset, 0, value.length);
|
||||
} else {
|
||||
stValue = Iconv.decode(value, info.collation.charset);
|
||||
}
|
||||
return "_binary'" + escapeString(stValue) + "'";
|
||||
} else if (typeof value.toSqlString === 'function') {
|
||||
return "'" + escapeString(String(value.toSqlString())) + "'";
|
||||
} else if (Array.isArray(value)) {
|
||||
let out = opts.arrayParenthesis ? '(' : '';
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
if (i !== 0) out += ',';
|
||||
out += escapeParameters(opts, info, value[i]);
|
||||
}
|
||||
if (opts.arrayParenthesis) out += ')';
|
||||
return out;
|
||||
} else {
|
||||
if (
|
||||
value.type != null &&
|
||||
[
|
||||
'Point',
|
||||
'LineString',
|
||||
'Polygon',
|
||||
'MultiPoint',
|
||||
'MultiLineString',
|
||||
'MultiPolygon',
|
||||
'GeometryCollection'
|
||||
].includes(value.type)
|
||||
) {
|
||||
//GeoJSON format.
|
||||
let prefix =
|
||||
info &&
|
||||
((info.isMariaDB() && info.hasMinVersion(10, 1, 4)) || (!info.isMariaDB() && info.hasMinVersion(5, 7, 6)))
|
||||
? 'ST_'
|
||||
: '';
|
||||
switch (value.type) {
|
||||
case 'Point':
|
||||
return prefix + "PointFromText('POINT(" + TextEncoder.geoPointToString(value.coordinates) + ")')";
|
||||
|
||||
case 'LineString':
|
||||
return (
|
||||
prefix + "LineFromText('LINESTRING(" + TextEncoder.geoArrayPointToString(value.coordinates) + ")')"
|
||||
);
|
||||
|
||||
case 'Polygon':
|
||||
return (
|
||||
prefix + "PolygonFromText('POLYGON(" + TextEncoder.geoMultiArrayPointToString(value.coordinates) + ")')"
|
||||
);
|
||||
|
||||
case 'MultiPoint':
|
||||
return (
|
||||
prefix +
|
||||
"MULTIPOINTFROMTEXT('MULTIPOINT(" +
|
||||
TextEncoder.geoArrayPointToString(value.coordinates) +
|
||||
")')"
|
||||
);
|
||||
|
||||
case 'MultiLineString':
|
||||
return (
|
||||
prefix +
|
||||
"MLineFromText('MULTILINESTRING(" +
|
||||
TextEncoder.geoMultiArrayPointToString(value.coordinates) +
|
||||
")')"
|
||||
);
|
||||
|
||||
case 'MultiPolygon':
|
||||
return (
|
||||
prefix + "MPolyFromText('MULTIPOLYGON(" + TextEncoder.geoMultiPolygonToString(value.coordinates) + ")')"
|
||||
);
|
||||
|
||||
case 'GeometryCollection':
|
||||
return (
|
||||
prefix +
|
||||
"GeomCollFromText('GEOMETRYCOLLECTION(" +
|
||||
TextEncoder.geometricCollectionToString(value.geometries) +
|
||||
")')"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (opts.permitSetMultiParamEntries) {
|
||||
let out = '';
|
||||
let first = true;
|
||||
for (let key in value) {
|
||||
const val = value[key];
|
||||
if (typeof val === 'function') continue;
|
||||
if (first) {
|
||||
first = false;
|
||||
} else {
|
||||
out += ',';
|
||||
}
|
||||
out += '`' + key + '`=';
|
||||
out += this.escape(val);
|
||||
}
|
||||
if (out === '') return "'" + escapeString(JSON.stringify(value)) + "'";
|
||||
return out;
|
||||
} else {
|
||||
return "'" + escapeString(JSON.stringify(value)) + "'";
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
return "'" + escapeString(value) + "'";
|
||||
}
|
||||
}
|
||||
|
||||
// see https://mariadb.com/kb/en/library/string-literals/
|
||||
const LITTERAL_ESCAPE = {
|
||||
'\u0000': '\\0',
|
||||
"'": "\\'",
|
||||
'"': '\\"',
|
||||
'\b': '\\b',
|
||||
'\n': '\\n',
|
||||
'\r': '\\r',
|
||||
'\t': '\\t',
|
||||
'\u001A': '\\Z',
|
||||
'\\': '\\\\'
|
||||
};
|
||||
|
||||
const CHARS_GLOBAL_REGEXP = /[\000\032"'\\\b\n\r\t]/g;
|
||||
|
||||
const escapeString = (val) => {
|
||||
let offset = 0;
|
||||
let escaped = '';
|
||||
let match;
|
||||
|
||||
while ((match = CHARS_GLOBAL_REGEXP.exec(val))) {
|
||||
escaped += val.substring(offset, match.index);
|
||||
escaped += LITTERAL_ESCAPE[match[0]];
|
||||
offset = CHARS_GLOBAL_REGEXP.lastIndex;
|
||||
}
|
||||
|
||||
if (offset === 0) {
|
||||
return val;
|
||||
}
|
||||
|
||||
if (offset < val.length) {
|
||||
escaped += val.substring(offset);
|
||||
}
|
||||
|
||||
return escaped;
|
||||
};
|
||||
|
||||
export { log, toHexString, escapeId, escapeParameters as escape };
|
||||
Reference in New Issue
Block a user