# -*- coding: binary -*-

module Rex
module Powershell
module Parser
  # Reserved special variables
  # Acquired with: Get-Variable | Format-Table name, value -auto
  RESERVED_VARIABLE_NAMES = [
    '$$',
    '$?',
    '$^',
    '$_',
    '$args',
    '$ConfirmPreference',
    '$ConsoleFileName',
    '$DebugPreference',
    '$Env',
    '$Error',
    '$ErrorActionPreference',
    '$ErrorView',
    '$ExecutionContext',
    '$false',
    '$FormatEnumerationLimit',
    '$HOME',
    '$Host',
    '$input',
    '$LASTEXITCODE',
    '$MaximumAliasCount',
    '$MaximumDriveCount',
    '$MaximumErrorCount',
    '$MaximumFunctionCount',
    '$MaximumHistoryCount',
    '$MaximumVariableCount',
    '$MyInvocation',
    '$NestedPromptLevel',
    '$null',
    '$OutputEncoding',
    '$PID',
    '$PROFILE',
    '$ProgressPreference',
    '$PSBoundParameters',
    '$PSCulture',
    '$PSEmailServer',
    '$PSHOME',
    '$PSSessionApplicationName',
    '$PSSessionConfigurationName',
    '$PSSessionOption',
    '$PSUICulture',
    '$PSVersionTable',
    '$PWD',
    '$ReportErrorShowExceptionClass',
    '$ReportErrorShowInnerException',
    '$ReportErrorShowSource',
    '$ReportErrorShowStackTrace',
    '$ShellId',
    '$StackTrace',
    '$true',
    '$VerbosePreference',
    '$WarningPreference',
    '$WhatIfPreference'
  ].map(&:downcase).freeze

  #
  # Get variable names from code, removes reserved names from return
  #
  # @return [Array] variable names
  def get_var_names
    our_vars = code.scan(/\$[a-zA-Z\-\_0-9]+/).uniq.flatten.map(&:strip)
    our_vars.select { |v| !RESERVED_VARIABLE_NAMES.include?(v.downcase) }
  end

  #
  # Get function names from code
  #
  # @return [Array] function names
  def get_func_names
    code.scan(/function\s([a-zA-Z\-\_0-9]+)/).uniq.flatten
  end

  #
  # Attempt to find string literals in PSH expression
  #
  # @return [Array] string literals
  def get_string_literals
    code.scan(/@"(.+?)"@|@'(.+?)'@/m)
  end

  #
  # Scan code and return matches with index
  #
  # @param str [String] string to match in code
  # @param source [String] source code to match, defaults to @code
  #
  # @return [Array[String,Integer]] matched items with index
  def scan_with_index(str, source = code)
    ::Enumerator.new do |y|
      source.scan(str) do
        y << ::Regexp.last_match
      end
    end.map { |m| [m.to_s, m.offset(0)[0]] }
  end

  #
  # Return matching bracket type
  #
  # @param char [String] opening bracket character
  #
  # @return [String] matching closing bracket
  def match_start(char)
    case char
    when '{'
      '}'
    when '('
      ')'
    when '['
      ']'
    when '<'
      '>'
    else
      fail ArgumentError, 'Unknown starting bracket'
    end
  end

  #
  # Extract block of code inside brackets/parenthesis
  #
  # Attempts to match the bracket at idx, handling nesting manually
  # Once the balanced matching bracket is found, all script content
  # between idx and the index of the matching bracket is returned
  #
  # @param idx [Integer] index of opening bracket
  #
  # @return [String] content between matching brackets
  def block_extract(idx)
    fail ArgumentError unless idx

    if idx < 0 || idx >= code.length
      fail ArgumentError, 'Invalid index'
    end

    start = code[idx]
    stop = match_start(start)
    delims = scan_with_index(/#{Regexp.escape(start)}|#{Regexp.escape(stop)}/, code[idx + 1..-1])
    delims.map { |x| x[1] = x[1] + idx + 1 }
    c = 1
    sidx = nil
    # Go through delims till we balance, get idx
    while (c != 0) && (x = delims.shift)
      sidx = x[1]
      x[0] == stop ? c -= 1 : c += 1
    end

    code[idx..sidx]
  end

  #
  # Extract a block of function code
  #
  # @param func_name [String] function name
  # @param delete [Boolean] delete the function from the code
  #
  # @return [String] function block
  def get_func(func_name, delete = false)
    start = code.index(func_name)

    return nil unless start

    idx = code[start..-1].index('{') + start
    func_txt = block_extract(idx)

    if delete
      delete_code = code[0..idx]
      delete_code << code[(idx + func_txt.length)..-1]
      @code = delete_code
    end

    Function.new(func_name, func_txt)
  end
end # Parser
end
end

