/**
Copyright: 2024 Contrast Security, Inc
  Contact: support@contrastsecurity.com
  License: Commercial

  NOTICE: This Software and the patented inventions embodied within may only be
  used as part of Contrast Security’s commercial offerings. Even though it is
  made available through public repositories, use of this Software is subject to
  the applicable End User Licensing Agreement found at
  https://www.contrastsecurity.com/enduser-terms-0317a or as otherwise agreed
  between Contrast Security and the End User. The Software may not be reverse
  engineered, modified, repackaged, sold, redistributed or otherwise used in a
  way not consistent with the End User License Agreement.
*/
'use strict';

/**
 * @module lib/rules/protect/sqli/sql-scanner/MySQLScanner
 */

const { BaseScanner } = require('../../base-scanner');
const scanStates = require('../../base-scanner/scan-states');

const { INSIDE_EXECUTABLE_BLOCK_COMMENT } = scanStates;
const { EXPECTING_TOKEN } = scanStates;

/**
 * SQL scanner for MySQL database queries.
 */
class MySQLScanner extends BaseScanner {
  /**
   * Override because this database has custom tokens -
   * "Executable comment blocks" e.g. /*! code ...
   * Calls super method if we don't need to process anything custom.
   * @param {ScanState} state
   * @returns {ScanState}
   */
  readAtCursor(state) {
    return this.isExecutableCommentStart(state) ||
      state.value() === INSIDE_EXECUTABLE_BLOCK_COMMENT
      ? this.processExecutableCommentState(state)
      : super.readAtCursor(state);
  }

  /**
   * Custom token processsing. Checks for database-specific tokens,
   * and processes them in our own methods.
   * @param   {ScanState} state   The scan state of the scanner.
   * @returns {ScanState}
   */
  processExecutableCommentState(state) {
    const { codeString: query, index: idx } = state;
    const behind1 = query[idx];
    const current = query[idx + 1];

    if (current === '!') {
      return state.flag('INCLUSIVE_CAPTURE', true).enter(EXPECTING_TOKEN);
    }

    if (behind1 === '*' && current === '/') {
      return state.flag('INCLUSIVE_CAPTURE', true).is(EXPECTING_TOKEN);
    }

    return state.is(INSIDE_EXECUTABLE_BLOCK_COMMENT);
  }

  /**
   * Override so we can account for being in nested state for MySQL's
   * block comments with executable tokens in their bodies.
   * https://dev.mysql.com/doc/refman/8.0/en/comments.html
   * @param {ScanState} state
   * @returns {ScanState}
   */
  processExpectingToken(state) {
    const { codeString: query, index: idx } = state;
    const atCursor = query[idx + 1];
    const ahead1 = query[idx + 2];

    if (state.nestedState() === INSIDE_EXECUTABLE_BLOCK_COMMENT) {
      if (atCursor === '*' && ahead1 === '/') {
        return state.exit();
      }
    }
    return super.processExpectingToken(state);
  }

  /**
   * Override so we can account for being in nested state for MySQL's
   * block comments with executable tokens in their bodies.
   * https://dev.mysql.com/doc/refman/8.0/en/comments.html
   * @param {ScanState} state
   * @returns {ScanState}
   */
  processInsideToken(state) {
    const { codeString: query, index: idx } = state;
    const atCursor = query[idx + 1];
    const ahead1 = query[idx + 2];

    if (state.nestedState() === INSIDE_EXECUTABLE_BLOCK_COMMENT) {
      if (atCursor === '*' && ahead1 === '/') {
        return state.exit();
      }
    }
    return super.processInsideToken(state);
  }

  /**
   * Checks whether the characters around cursor signify a line comment start.
   * @param   {ScanState} state   The current scan state of the parser.
   * @returns {Boolean}
   */
  isLineCommentStart({ codeString: query, index: idx }) {
    return (
      (query && query[idx + 1] === '#') ||
      super.isLineCommentStart({ codeString: query, index: idx })
    );
  }

  /**
   * Whether the characters at cursor signify the start of a block comment.
   * The character sequence "/*!" starts an inline comment that has code that's
   * executed in MySQL,
   * so we say we're not in a comment
   * @param   {ScanState} state   The current scan state of the parser.
   * @returns {Boolean}
   */
  isBlockCommentStart({ codeString: query, index: idx }) {
    return (
      query &&
      query[idx + 1] === '/' &&
      query[idx + 2] === '*' &&
      query[idx + 3] !== '!'
    );
  }

  /**
   * The only caveat to BaseSQLScanner logic is MySQL's code-embedded comments.
   * @param   {ScanState} state   The current scan state of the parser.
   * @returns {Boolean}
   */
  isOperator({ codeString: query, index: idx }) {
    if (!query) {
      return false;
    }

    const prev = query[idx];
    const first = query[idx + 1];
    if ((prev === '/' && first === '*') || (prev === '*' && first === '!')) {
      return false;
    }

    return super.isOperator({ codeString: query, index: idx });
  }

  /**
   * Checks whether the current cursor current state is the start of a MySQL
   * executable comment block.
   * @param   {ScanState} state   The current scan state of the parser.
   * @returns {Boolean}
   */
  isExecutableCommentStart({ codeString: query, index: idx }) {
    const current = query[idx + 1];
    const next = query[idx + 2];
    const followedBy = query[idx + 3];

    return current === '/' && next === '*' && followedBy === '!';
  }
}

module.exports = MySQLScanner;
