Ruby: анализ, замена и оценка строковой формулы

Я создаю простое приложение для опроса Ruby on Rails для проекта психологического опроса друга. Итак, у нас есть опросы, в каждом опросе есть куча вопросов, и у каждого вопроса есть один из вариантов, который могут выбрать участники. Ничего захватывающего.

Одним из интересных аспектов является то, что каждый вариант ответа имеет связанное с ним значение балла. Таким образом, для каждого опроса необходимо рассчитать общий балл на основе этих значений.

Теперь моя идея состоит в том, чтобы вместо жестко закодированных расчетов позволить пользователю добавить формулу, по которой будет рассчитываться общий балл опроса. Примеры формул:

"Q1 + Q2 + Q3"
"(Q1 + Q2 + Q3) / 3"
"(10 - Q1) + Q2 + (Q3 * 2)"

Так что просто базовая математика (с некоторыми дополнительными скобками для ясности). Идея состоит в том, чтобы формулы были очень простыми, чтобы любой, кто владеет базовой математикой, мог вводить их, не прибегая к причудливому синтаксису.

Моя идея состоит в том, чтобы взять любую заданную формулу и заменить заполнители, такие как Q1, Q2 и т. д., на значения баллов, основанные на том, что выбирает участник. И затем eval() только что сформированная строка. Что-то вроде этого:

f = "(Q1 + Q2 + Q3) / 2"  # some crazy formula for this survey
values = {:Q1 => 1, :Q2 => 2, :Q3 => 2}  # values for substitution 
result = f.gsub(/(Q\d+)/) {|m| values[$1.to_sym] }   # string to be eval()-ed
eval(result)

Итак, мои вопросы:

  1. Есть лучший способ это сделать? Я открыт для любых предложений.

  2. Как обрабатывать формулы, в которых не все заполнители были успешно заменены (например, не был дан ответ на один вопрос)? Пример: {:Q2 => 2} не было в хэше значений? Моя идея состояла в том, чтобы спасти eval(), но в этом случае это не подведет, потому что (1 + + 2) / 2 все еще может быть обработано eval()... есть мысли?

  3. Как добиться должного результата? Должно быть 2,5, но из-за целочисленной арифметики оно будет усечено до 2. Я не могу ожидать, что люди, предоставившие правильную формулу (например, / 2,0 ), поймут этот нюанс.

  4. Я не ожидаю этого, но как лучше всего защитить eval() от злоупотреблений (например, неверная формула, ввод манипулируемых значений)? Пример: f = 'system("ruby -v"); (Q1 + (Q2 / 3) + Q3 + (Q4 * 2)) / 2 '

Благодарю вас!


person Swartz    schedule 02.02.2011    source источник
comment
Может ли кто-нибудь добавить хотя бы несколько примеров Treetop, которые помогут мне начать работу. Я немного перегружен всем тем, что прочитал на Treetop. Или мне начать новый вопрос? Это становится слишком сложным, так как я просто надеялся собрать что-то вместе. Ни для какого долголетия. вздох   -  person Swartz    schedule 03.02.2011


Ответы (5)


Хорошо, теперь это абсолютно безопасно. Клянусь!

Обычно я бы клонировал переменную formula, но в этом случае, поскольку вы беспокоитесь о враждебном пользователе, я очистил переменную на месте:

class Evaluator

  def self.formula(formula, values)
    # remove anything but Q's, numbers, ()'s, decimal points, and basic math operators 
    formula.gsub!(/((?![qQ0-9\s\.\-\+\*\/\(\)]).)*/,'').upcase!
    begin
      formula.gsub!(/Q\d+/) { |match|
        ( 
          values[match.to_sym] && 
          values[match.to_sym].class.ancestors.include?(Numeric) ?
          values[match.to_sym].to_s :
          '0'
        )+'.0'
      }
      instance_eval(formula)
    rescue Exception => e
      e.inspect
    end
  end

end

f = '(q1 + (q2 / 3) + q3 + (q4 * 2))'  # some crazy formula for this survey
values = {:Q2 => 1, :Q4 => 2}  # values for substitution 
puts "formula: #{f} = #{Evaluator.formula(f,values)}"  
=> formula: (0.0 + (1.0 / 3) + 0.0 + (2.0 * 2)) = 4.333333333333333

f = '(Q1 + (Q2 / 3) + Q3 + (Q4 * 2)) / 2'  # some crazy formula for this survey
values = {:Q1 => 1, :Q3 => 2}  # values for substitution 
puts "formula: #{f} = #{Evaluator.formula(f,values)}"  
=> formula: (1.0 + (0.0 / 3) + 2.0 + (0.0 * 2)) / 2 = 1.5

f = '(Q1 + (Q2 / 3) + Q3 + (Q4 * 2)) / 2'  # some crazy formula for this survey
values = {:Q1 => 'delete your hard drive', :Q3 => 2}  # values for substitution 
puts "formula: #{f} = #{Evaluator.formula(f,values)}"  
=> formula: (0.0 + (0.0 / 3) + 2.0 + (0.0 * 2)) / 2 = 1.0

f = 'system("ruby -v")'  # some crazy formula for this survey
values = {:Q1 => 'delete your hard drive', :Q3 => 2}  # values for substitution 
puts "formula: #{f} = #{Evaluator.formula(f,values)}"  
=> formula: ( -) = #<SyntaxError: (eval):1: syntax error, unexpected ')'>
person Community    schedule 03.02.2011
comment
Это позаботится о вопросах № 2 и № 3. есть еще этот ужасный eval(). - person Swartz; 03.02.2011
comment
Спасибо за ваш вклад. Я думаю, вы пропустили то, на что указали cam и Phrogz. Вы предотвращаете внедрение в заполнители, такие как Q1, Q2 и т. д. Однако попробуйте использовать эту формулу: f = 'system("ruby -v"); (Q1 + (Q2 / 3) + Q3 + (Q4 * 2)) / 2 ' . Если вы eval(), вы сможете увидеть установленную версию ruby. Вы можете себе представить, что еще можно сделать. Это предполагает наличие злого умысла со стороны лица, редактирующего формулу. Маловероятно в моем сценарии, но я хочу избежать этого потенциального злоупотребления, поскольку он размещен на общедоступном сервере. Думаю, единственный верный способ - это разбор грамматик. Я надеялся избежать этого. вздох - person Swartz; 03.02.2011
comment
Да, это $1 и тому подобное - моя старая привычка со времен Perl. - person Swartz; 03.02.2011

Возможно, это не стоит усилий, но если бы мне пришлось это делать, я бы использовал Treetop для определения грамматика разбора. Существуют даже примеры использования таких грамматик в стиле PEG для простой арифметики, так что вы будете на 90 % использовать грамматику и большую часть пути к оценке взвешивания.

person Phrogz    schedule 02.02.2011
comment
Посмотрел страницу Treetop. Казалось бы, здесь перебор. - person Swartz; 03.02.2011
comment
Наверняка это может быть излишним; однако это один из способов гарантировать, что ваша среда не будет скомпрометирована вредоносным вводом. Достаточно одного человека, который введет exec("rm / -rf") (или что-то аналогичное разрушительное, но доступное для вашего веб-сервера), чтобы нанести ущерб. - person Phrogz; 03.02.2011
comment
@Swartz: создание синтаксического анализатора - это путь. Если вы использовали что-то вроде Treetop, ваша четвертая проблема полностью исчезает. Ваш третий вопрос становится тривиальным. Вопрос №2 уже не актуален (это будет простая деталь реализации). - person cam; 03.02.2011

Используйте Dentaku:

Dentaku — это синтаксический анализатор и оценщик для языка математических и логических формул, который позволяет во время выполнения связывать значения с переменными, на которые ссылаются формулы. Он предназначен для безопасной оценки ненадежных выражений, не открывая дыр в безопасности.

person rossmeissl    schedule 23.09.2014

Вы можете использовать RubyParser для интерпретации выражения e, итерируемого узлами, чтобы проверить, существует ли какой-либо опасный код, например вызов функции. Смотреть:

require 'ruby_parser'
def valid_formula?(str, consts=[])
  !!valid_formula_node?(RubyParser.new.process(str), consts)
rescue Racc::ParseError
  false
end
def valid_formula_node?(node, consts)
  case node.shift
  when :call
    node[1].to_s !~ /^[a-z_0-9]+$/i and
    valid_formula_node?(node[0], consts) and
    valid_formula_node?(node[2], consts)
  when :arglist
    node.all? {|inner| valid_formula_node?(inner, consts) }
  when :lit
    Numeric === node[0]
  when :const
    consts.include? node[0]
  end
end

Это просто разрешает операторы, числа и определенные константы.

valid_formula?("(Q1 + Q2 + Q3) / 2", [:Q1, :Q2, :Q3]) #=> true
valid_formula?("exit!", [:Q1, :Q2, :Q3])              #=> false
valid_formula?("!(%&$)%*", [:Q1, :Q2, :Q3])           #=> false
person Guilherme Bernal    schedule 02.02.2011

Касательно 2) Несмотря на то, что это уродливо, вы можете просто создать хэш со значениями по умолчанию и убедиться, что это не работает, когда на нем вызывается to_s (я же сказал, что это уродливо, верно?):

>> class NaN ; def to_s; raise ArgumentError ; end; end #=> nil
>> h = Hash.new { NaN.new } #=> {}
>> h[:q1] = 12 #=> 12
>> h[:q1] #=> 12
>> h[:q2]
ArgumentError: ArgumentError

Re 3) Просто убедитесь, что в вашем расчете есть хотя бы один поплавок. Самый простой способ - просто превратить все предоставленные значения в числа с плавающей запятой во время замены:

>> result = f.gsub(/(Q\d+)/) {|m| values[$1.to_sym].to_f } #=> "(1.0 + 2.0 + 2.0) / 2"
>> eval result #=> 2.5

Касательно 4) вы можете прочитать о $SAFE. «Кирка» на самом деле содержит пример evalввода чего-либо, введенного в веб-форму:

http://ruby-doc.org/docs/ProgrammingRuby/html/taint.html

Это если вы действительно хотите пойти по маршруту eval, не игнорируйте альтернативы, представленные в этом обсуждении.

person Michael Kohl    schedule 03.02.2011