# frozen_string_literal: true
#
# ronin-code-sql - A Ruby DSL for crafting SQL Injections.
#
# Copyright (c) 2007-2023 Hal Brodigan (postmodern.mod3 at gmail.com)
#
# ronin-code-sql is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# ronin-code-sql is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with ronin-code-sql. If not, see .
#
require 'ronin/support/encoding/sql'
module Ronin
module Code
module SQL
#
# Generates raw SQL.
#
# @api private
#
class Emitter
# The case to use when emitting keywords
#
# @return [:lower, :upper, :random, nil]
attr_reader :case
# String to use for white-space
#
# @return [String]
attr_reader :space
# Type of String quotes to use
#
# @return [:single, :double]
attr_reader :quotes
# Generate DB-specific code
#
# @return [nil, :mysql, :postgres, :oracle, :mssql]
attr_reader :syntax
# String to use as 'comment' or `nil` to let the emitter decide
#
# @return [String, nil]
attr_reader :comment
#
# Initializes the SQL Emitter.
#
# @param [String] space
# String to use for white-space.
#
# @param [:single, :double] quotes
# Type of quotes to use for Strings.
#
# @param [nil, :mysql, :postgres, :oracle, :mssql] syntax
# Syntax used during code-generation
#
# @param [nil, String] comment
# String to use as the comment when terminating injection string
#
# @param [Hash{Symbol => Object}] kwargs
# Emitter options.
#
# @option kwargs [:lower, :upper, :random, nil] :case
# Case for keywords.
#
def initialize(space: ' ', quotes: :single, syntax: nil, comment: nil, **kwargs)
@case = kwargs[:case] # HACK: because `case` is a ruby keyword
@syntax = syntax
@comment = comment
@space = space
@quotes = quotes
end
#
# Emits a SQL keyword.
#
# @param [Symbol, Array] keyword
# The SQL keyword.
#
# @return [String]
# The raw SQL.
#
def emit_keyword(keyword)
string = Array(keyword).join(@space)
case @case
when :upper then string.upcase
when :lower then string.downcase
when :random
string.tap do
(string.length / 2).times do
index = rand(string.length)
string[index] = string[index].swapcase
end
end
else
string
end
end
#
# Emits a SQL operator.
#
# @param [Array, Symbol] operator
# The operator symbol.
#
# @return [String]
# The raw SQL.
#
def emit_operator(operator)
case operator
when /^\W+$/ then operator.to_s
else emit_keyword(operator)
end
end
#
# Emits the `NULL` value.
#
# @return [String]
#
def emit_null
emit_keyword(:NULL)
end
#
# Emits a `false` value.
#
# @return [String]
# The raw SQL.
#
def emit_false
"1=0"
end
#
# Emits a `true` value.
#
# @return [String]
# The raw SQL.
#
def emit_true
"1=1"
end
#
# Emits a SQL comment.
#
# @return [String]
# The raw SQL.
#
# @since 2.1.0
#
def emit_comment
# Return chosen comment or default one which works everywhere
@comment || '-- '
end
#
# Emits a SQL Integer.
#
# @param [Integer] int
# The Integer.
#
# @return [String]
# The raw SQL.
#
def emit_integer(int)
int.to_s
end
#
# Emits a SQL Decimal.
#
# @param [Float] decimal
# The decimal.
#
# @return [String]
# The raw SQL.
#
def emit_decimal(decimal)
decimal.to_s
end
#
# Emits a SQL String.
#
# @param [String] string
# The String.
#
# @return [String]
# The raw SQL.
#
def emit_string(string)
Support::Encoding::SQL.escape(string, quotes: @quotes)
end
#
# Emits a SQL field.
#
# @param [Field, Symbol, String] field
# The SQL field.
#
# @return [String]
# The raw SQL.
#
def emit_field(field)
name = emit_keyword(field.name)
if field.parent
name = "#{emit_field(field.parent)}.#{name}"
end
return name
end
#
# Emits a list of elements.
#
# @param [#map] list
# The list of elements.
#
# @return [String]
# The raw SQL.
#
def emit_list(list)
"(#{list.map { |element| emit(element) }.join(',')})"
end
#
# Emits a list of columns and assigned values.
#
# @param [Hash{Field,Symbol => Object}] values
# The column names and values.
#
# @return [String]
# The raw SQL.
#
def emit_assignments(values)
values.map { |key,value|
"#{emit_keyword(key)}=#{emit(value)}"
}.join(',')
end
#
# Emits a value used in an expression.
#
# @param [Statement, #to_sql] operand
# The operand to emit.
#
# @return [String]
# The raw SQL.
#
# @since 1.1.0
#
def emit_argument(operand)
case operand
when Statement then "(#{emit_statement(operand)})"
else emit(operand)
end
end
#
# Emits a SQL expression.
#
# @param [BinaryExpr, UnaryExpr] expr
# The SQL expression.
#
# @return [String]
# The raw SQL.
#
def emit_expression(expr)
op = emit_operator(expr.operator)
case expr
when BinaryExpr
left = emit_argument(expr.left)
right = emit_argument(expr.right)
case op
when /^\W+$/ then "#{left}#{op}#{right}"
else [left, op, right].join(@space)
end
when UnaryExpr
operand = emit_argument(expr.operand)
case expr.operator
when /^\W+$/ then "#{op}#{operand}"
else [op, operand].join(@space)
end
end
end
#
# Emits a SQL function.
#
# @param [Function] function
# The SQL function.
#
# @return [String]
# The raw SQL.
#
def emit_function(function)
name = emit_keyword(function.name)
arguments = function.arguments.map { |value| emit_argument(value) }
return "#{name}(#{arguments.join(',')})"
end
#
# Emits a SQL object.
#
# @param [#to_sql] object
# The SQL object.
#
# @return [String]
# The raw SQL.
#
# @raise [ArgumentError]
# Could not emit an unknown SQL object.
#
def emit(object)
case object
when NilClass then emit_null
when TrueClass then emit_true
when FalseClass then emit_false
when Integer then emit_integer(object)
when Float then emit_decimal(object)
when String then emit_string(object)
when Literal then emit(object.value)
when Symbol then emit_keyword(object)
when Field then emit_field(object)
when Array then emit_list(object)
when Hash then emit_assignments(object)
when BinaryExpr, UnaryExpr then emit_expression(object)
when Function then emit_function(object)
when Clause then emit_clause(object)
when Statement then emit_statement(object)
when StatementList then emit_statement_list(object)
else
if object.respond_to?(:to_sql)
object.to_sql
else
raise(ArgumentError,"cannot emit #{object.class}")
end
end
end
#
# Emits a SQL Clause.
#
# @param [Clause] clause
# The SQL Clause.
#
# @return [String]
# The raw SQL.
#
def emit_clause(clause)
sql = emit_keyword(clause.keyword)
unless clause.argument.nil?
sql << @space << emit_argument(clause.argument)
end
return sql
end
#
# Emits multiple SQL Clauses.
#
# @param [Array] clauses
# The clauses to emit.
#
# @return [String]
# The emitted clauses.
#
def emit_clauses(clauses)
clauses.map { |clause| emit_clause(clause) }.join(@space)
end
#
# Emits a SQL Statement.
#
# @param [Statement] stmt
# The SQL Statement.
#
# @return [String]
# The raw SQL.
#
def emit_statement(stmt)
sql = emit_keyword(stmt.keyword)
unless stmt.argument.nil?
sql << @space << case stmt.argument
when Array
if stmt.argument.length == 1
emit_argument(stmt.argument[0])
else
emit_list(stmt.argument)
end
else
emit_argument(stmt.argument)
end
end
unless stmt.clauses.empty?
sql << @space << emit_clauses(stmt.clauses)
end
return sql
end
#
# Emits a full SQL statement list.
#
# @param [StatementList] list
# The SQL statement list.
#
# @return [String]
# The raw SQL.
#
def emit_statement_list(list)
list.statements.map { |stmt|
emit_statement(stmt)
}.join(";#{@space}")
end
end
end
end
end