UNPKG

mariadb

Version:
530 lines (477 loc) 15 kB
// SPDX-License-Identifier: LGPL-2.1-or-later // Copyright (c) 2015-2025 MariaDB Corporation Ab const Errors = require('../misc/errors'); const State = { Normal: 1 /* inside 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 */ module.exports.splitQuery = function (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; } 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 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 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}} */ module.exports.splitQueryPlaceholder = function (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; } 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 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.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 }; }; module.exports.searchPlaceholder = function (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; break; case '#': if (state === State.Normal) state = State.EOLComment; break; case '-': if (state === State.Normal && lastChar === '-') { 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 */ module.exports.validateFileName = function (sql, parameters, fileName) { // in case of windows, file name in query are escaped // so for example LOAD DATA LOCAL INFILE 'C:\\Temp\\myFile.txt' ... // but server return '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 ';' * (last one is not parsed) * * @param bufState buffer * @returns {*[]} array of queries contained in buffer */ module.exports.parseQueries = function (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; };