/**
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';

const COMMAND_STATES = {
  Command: 0,
  Quote: 1,
  DoubleQuote: 2,
  Comment: 3,
  Arithmetic: 4
};

module.exports = class ChainedCommandScanner {
  constructor({ command }) {
    this.command = command;
    this.stack = [COMMAND_STATES.Command];
    this.offset = 0;
  }

  /**
   * Runs the chained commands state machine
   *
   * @returns the starting index of a chained command, or -1 if no such index
   */
  findChaining() {
    if (!this.command) {
      return -1;
    }

    let stateFunc;
    while (this.offset < this.command.length) {
      stateFunc = null;
      switch (this.stack[this.stack.length - 1]) {
        case COMMAND_STATES.Command:
          stateFunc = this.handleCommandState;
          break;
        case COMMAND_STATES.Quote:
          stateFunc = this.handleSingleQuoteState;
          break;
        case COMMAND_STATES.DoubleQuote:
          stateFunc = this.handleDoubleQuoteState;
          break;
        case COMMAND_STATES.Comment:
          stateFunc = this.handleCommentState;
          break;
        case COMMAND_STATES.Arithmetic:
          stateFunc = this.handleArithmeticState;
          break;
      }
      if (stateFunc.apply(this)) {
        return this.offset;
      }
    }

    return -1;
  }

  /**
   * Check if the char at the current offset is a separator
   *
   * @returns boolean
   */
  isSeparator() {
    return ['&', ';', '>', '<', '|', '\n'].includes(this.command[this.offset]);
  }

  /**
   * Check if the chars at the current offset is the start of command substitution
   *
   * @returns boolean
   */
  isSubstitutionStart() {
    if (this.command[this.offset] == '`') {
      return true;
    }

    if (
      this.command.length - this.offset >= 2 &&
      this.command.substring(this.offset, this.offset + 2) === '$(' &&
      !this.isArithmeticStart()
    ) {
      return true;
    }

    return false;
  }

  /**
   * Check if the chars at the current offset is the start of arithmetic substitution
   *
   * @returns boolean
   */
  isArithmeticStart() {
    if (
      this.command.length - this.offset >= 3 &&
      this.command.substring(this.offset, this.offset + 3) === '$(('
    ) {
      return true;
    }

    return false;
  }

  /**
   * Check if the chars at the current offset is the end of arithmetic substitution
   *
   * @returns boolean
   */
  isArithmeticEnd() {
    if (
      this.command.length - this.offset >= 2 &&
      this.command.substring(this.offset, this.offset + 2) === '))'
    ) {
      return true;
    }

    return false;
  }

  /**
   * Check if the char at the current offset is a single quote
   *
   * @returns boolean
   */
  isSingleQuote() {
    return this.command[this.offset] === "'";
  }

  /**
   * Check if the char at the current offset is a double quote
   *
   * @returns boolean
   */
  isDoubleQuote() {
    return this.command[this.offset] === '"';
  }

  /**
   * Check if the char at the current offset is the start of a comment
   *
   * @returns boolean
   */
  isComment() {
    return this.command[this.offset] === '#';
  }

  /**
   * Check if the char at the current offset is a newline
   *
   * @returns boolean
   */
  isNewline() {
    return this.command[this.offset] === '\n';
  }

  /**
   * Finds the state to transition to from Command
   *
   * @returns integer - the next state
   */
  getNextCommandState() {
    if (this.isSingleQuote()) {
      return COMMAND_STATES.Quote;
    }
    if (this.isDoubleQuote()) {
      return COMMAND_STATES.DoubleQuote;
    }
    if (this.isComment()) {
      return COMMAND_STATES.Comment;
    }
    if (this.isArithmeticStart()) {
      return COMMAND_STATES.Arithmetic;
    }

    return COMMAND_STATES.Command;
  }

  /**
   * Manipulate state stack and offset when in Command state
   *
   * @returns boolean - true if a chained command is found, false otherwise
   */
  handleCommandState() {
    if (this.isSeparator() || this.isSubstitutionStart()) {
      return true;
    }

    const nextState = this.getNextCommandState();
    if (nextState != COMMAND_STATES.Command) {
      this.stack.push(nextState);
    }

    if (nextState === COMMAND_STATES.Arithmetic) {
      this.offset += 3;
    } else {
      this.offset++;
    }
    return false;
  }

  /**
   * Manipulate state stack and offset when in SingleQuote state
   *
   * @returns boolean - true if a chained command is found, false otherwise
   */
  handleSingleQuoteState() {
    if (this.isSingleQuote()) {
      this.stack.pop();
    }
    this.offset++;
    return false;
  }

  /**
   * Manipulate state stack and offset when in DoubleQuote state
   *
   * @returns boolean - true if a chained command is found, false otherwise
   */
  handleDoubleQuoteState() {
    if (this.isSubstitutionStart()) {
      return true;
    }
    if (this.isDoubleQuote()) {
      this.stack.pop();
    } else if (this.isArithmeticStart()) {
      this.stack.push(COMMAND_STATES.Arithmetic);
      this.offset += 3;
      return false;
    }
    this.offset++;
    return false;
  }

  /**
   * Manipulate state stack and offset when in Comment state
   *
   * @returns boolean - true if a chained command is found, false otherwise
   */
  handleCommentState() {
    if (this.isNewline()) {
      return true;
    }
    this.offset++;
    return false;
  }

  /**
   * Manipulate state stack and offset when in Arithmetic state
   *
   * @returns boolean - true if a chained command is found, false otherwise
   */
  handleArithmeticState() {
    if (this.isSubstitutionStart()) {
      return true;
    }
    if (this.isDoubleQuote()) {
      this.stack.push(COMMAND_STATES.DoubleQuote);
    } else if (this.isArithmeticEnd()) {
      this.stack.pop();
      this.offset += 2;
      return false;
    }
    this.offset++;
    return false;
  }
};
