diff --git a/lib/liquid.rb b/lib/liquid.rb index d6334ec..393454f 100644 --- a/lib/liquid.rb +++ b/lib/liquid.rb @@ -63,6 +63,7 @@ require 'liquid/variable' require 'liquid/variable_lookup' require 'liquid/range_lookup' require 'liquid/file_system' +require 'liquid/resource_limits' require 'liquid/template' require 'liquid/standardfilters' require 'liquid/condition' diff --git a/lib/liquid/block_body.rb b/lib/liquid/block_body.rb index 5e8a965..371e058 100644 --- a/lib/liquid/block_body.rb +++ b/lib/liquid/block_body.rb @@ -69,8 +69,7 @@ module Liquid def render(context) output = [] - context.resource_limits[:render_length_current] = 0 - context.resource_limits[:render_score_current] += @nodelist.length + context.resource_limits.render_score += @nodelist.length @nodelist.each do |token| # Break out if we have any unhanded interrupts. @@ -104,12 +103,13 @@ module Liquid def render_token(token, context) token_output = (token.respond_to?(:render) ? token.render(context) : token) - context.increment_used_resources(:render_length_current, token_output) - if context.resource_limits_reached? - context.resource_limits[:reached] = true + token_str = token_output.is_a?(Array) ? token_output.join : token_output.to_s + + context.resource_limits.render_length += token_str.length + if context.resource_limits.reached? raise MemoryError.new("Memory limits exceeded".freeze) end - token_output + token_str end def create_variable(token, options) diff --git a/lib/liquid/context.rb b/lib/liquid/context.rb index 179a466..ab19071 100644 --- a/lib/liquid/context.rb +++ b/lib/liquid/context.rb @@ -21,9 +21,7 @@ module Liquid @scopes = [(outer_scope || {})] @registers = registers @errors = [] - @resource_limits = resource_limits || Template.default_resource_limits.dup - @resource_limits[:render_score_current] = 0 - @resource_limits[:assign_score_current] = 0 + @resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits) squash_instance_assigns_with_environments @this_stack_used = false @@ -36,20 +34,6 @@ module Liquid @filters = [] end - def increment_used_resources(key, obj) - @resource_limits[key] += if obj.kind_of?(String) || obj.kind_of?(Array) || obj.kind_of?(Hash) - obj.length - else - 1 - end - end - - def resource_limits_reached? - (@resource_limits[:render_length_limit] && @resource_limits[:render_length_current] > @resource_limits[:render_length_limit]) || - (@resource_limits[:render_score_limit] && @resource_limits[:render_score_current] > @resource_limits[:render_score_limit] ) || - (@resource_limits[:assign_score_limit] && @resource_limits[:assign_score_current] > @resource_limits[:assign_score_limit] ) - end - def strainer @strainer ||= Strainer.create(self, @filters) end diff --git a/lib/liquid/resource_limits.rb b/lib/liquid/resource_limits.rb new file mode 100644 index 0000000..190685e --- /dev/null +++ b/lib/liquid/resource_limits.rb @@ -0,0 +1,23 @@ +module Liquid + class ResourceLimits + attr_accessor :render_length, :render_score, :assign_score, + :render_length_limit, :render_score_limit, :assign_score_limit + + def initialize(limits) + @render_length_limit = limits[:render_length_limit] + @render_score_limit = limits[:render_score_limit] + @assign_score_limit = limits[:assign_score_limit] + reset + end + + def reached? + (@render_length_limit && @render_length > @render_length_limit) || + (@render_score_limit && @render_score > @render_score_limit ) || + (@assign_score_limit && @assign_score > @assign_score_limit ) + end + + def reset + @render_length = @render_score = @assign_score = 0 + end + end +end diff --git a/lib/liquid/tags/assign.rb b/lib/liquid/tags/assign.rb index cda3878..f1dfb19 100644 --- a/lib/liquid/tags/assign.rb +++ b/lib/liquid/tags/assign.rb @@ -25,7 +25,10 @@ module Liquid def render(context) val = @from.render(context) context.scopes.last[@to] = val - context.increment_used_resources(:assign_score_current, val) + + inc = val.instance_of?(String) || val.instance_of?(Array) || val.instance_of?(Hash) ? val.length : 1 + context.resource_limits.assign_score += inc + ''.freeze end diff --git a/lib/liquid/tags/capture.rb b/lib/liquid/tags/capture.rb index 3ec0d67..6ec8a71 100644 --- a/lib/liquid/tags/capture.rb +++ b/lib/liquid/tags/capture.rb @@ -25,7 +25,7 @@ module Liquid def render(context) output = super context.scopes.last[@to] = output - context.increment_used_resources(:assign_score_current, output) + context.resource_limits.assign_score += output.length ''.freeze end diff --git a/lib/liquid/template.rb b/lib/liquid/template.rb index e86dfb5..c8ee0f8 100644 --- a/lib/liquid/template.rb +++ b/lib/liquid/template.rb @@ -18,7 +18,9 @@ module Liquid :locale => I18n.new } - attr_accessor :root, :resource_limits + attr_accessor :root + attr_reader :resource_limits + @@file_system = BlankFileSystem.new class TagRegistry @@ -110,7 +112,7 @@ module Liquid end def initialize - @resource_limits = self.class.default_resource_limits.dup + @resource_limits = ResourceLimits.new(self.class.default_resource_limits) end # Parse source code. @@ -203,6 +205,9 @@ module Liquid context.add_filters(args.pop) end + # Retrying a render resets resource usage + context.resource_limits.reset + begin # render the nodelist. # for performance reasons we get an array back here. join will make a string out of it. diff --git a/test/integration/template_test.rb b/test/integration/template_test.rb index e395206..e0fc528 100644 --- a/test/integration/template_test.rb +++ b/test/integration/template_test.rb @@ -93,69 +93,92 @@ class TemplateTest < Minitest::Test def test_resource_limits_works_with_custom_length_method t = Template.parse("{% assign foo = bar %}") - t.resource_limits = { :render_length_limit => 42 } + t.resource_limits.render_length_limit = 42 assert_equal "", t.render!("bar" => SomethingWithLength.new) end def test_resource_limits_render_length t = Template.parse("0123456789") - t.resource_limits = { :render_length_limit => 5 } + t.resource_limits.render_length_limit = 5 assert_equal "Liquid error: Memory limits exceeded", t.render() - assert t.resource_limits[:reached] - t.resource_limits = { :render_length_limit => 10 } + assert t.resource_limits.reached? + + t.resource_limits.render_length_limit = 10 assert_equal "0123456789", t.render!() - refute_nil t.resource_limits[:render_length_current] + refute_nil t.resource_limits.render_length end def test_resource_limits_render_score t = Template.parse("{% for a in (1..10) %} {% for a in (1..10) %} foo {% endfor %} {% endfor %}") - t.resource_limits = { :render_score_limit => 50 } + t.resource_limits.render_score_limit = 50 assert_equal "Liquid error: Memory limits exceeded", t.render() - assert t.resource_limits[:reached] + assert t.resource_limits.reached? + t = Template.parse("{% for a in (1..100) %} foo {% endfor %}") - t.resource_limits = { :render_score_limit => 50 } + t.resource_limits.render_score_limit = 50 assert_equal "Liquid error: Memory limits exceeded", t.render() - assert t.resource_limits[:reached] - t.resource_limits = { :render_score_limit => 200 } + assert t.resource_limits.reached? + + t.resource_limits.render_score_limit = 200 assert_equal (" foo " * 100), t.render!() - refute_nil t.resource_limits[:render_score_current] + refute_nil t.resource_limits.render_score end def test_resource_limits_assign_score t = Template.parse("{% assign foo = 42 %}{% assign bar = 23 %}") - t.resource_limits = { :assign_score_limit => 1 } + t.resource_limits.assign_score_limit = 1 assert_equal "Liquid error: Memory limits exceeded", t.render() - assert t.resource_limits[:reached] - t.resource_limits = { :assign_score_limit => 2 } + assert t.resource_limits.reached? + + t.resource_limits.assign_score_limit = 2 assert_equal "", t.render!() - refute_nil t.resource_limits[:assign_score_current] + refute_nil t.resource_limits.assign_score end def test_resource_limits_aborts_rendering_after_first_error t = Template.parse("{% for a in (1..100) %} foo1 {% endfor %} bar {% for a in (1..100) %} foo2 {% endfor %}") - t.resource_limits = { :render_score_limit => 50 } + t.resource_limits.render_score_limit = 50 assert_equal "Liquid error: Memory limits exceeded", t.render() - assert t.resource_limits[:reached] + assert t.resource_limits.reached? end def test_resource_limits_hash_in_template_gets_updated_even_if_no_limits_are_set t = Template.parse("{% for a in (1..100) %} {% assign foo = 1 %} {% endfor %}") t.render!() - assert t.resource_limits[:assign_score_current] > 0 - assert t.resource_limits[:render_score_current] > 0 - assert t.resource_limits[:render_length_current] > 0 + assert t.resource_limits.assign_score > 0 + assert t.resource_limits.render_score > 0 + assert t.resource_limits.render_length > 0 + end + + def test_render_length_persists_between_blocks + t = Template.parse("{% if true %}aaaa{% endif %}") + t.resource_limits.render_length_limit = 7 + assert_equal "Liquid error: Memory limits exceeded", t.render() + t.resource_limits.render_length_limit = 8 + assert_equal "aaaa", t.render() + + t = Template.parse("{% if true %}aaaa{% endif %}{% if true %}bbb{% endif %}") + t.resource_limits.render_length_limit = 13 + assert_equal "Liquid error: Memory limits exceeded", t.render() + t.resource_limits.render_length_limit = 14 + assert_equal "aaaabbb", t.render() + + t = Template.parse("{% if true %}a{% endif %}{% if true %}b{% endif %}{% if true %}a{% endif %}{% if true %}b{% endif %}{% if true %}a{% endif %}{% if true %}b{% endif %}") + t.resource_limits.render_length_limit = 5 + assert_equal "Liquid error: Memory limits exceeded", t.render() + t.resource_limits.render_length_limit = 11 + assert_equal "Liquid error: Memory limits exceeded", t.render() + t.resource_limits.render_length_limit = 12 + assert_equal "ababab", t.render() end def test_default_resource_limits_unaffected_by_render_with_context context = Context.new t = Template.parse("{% for a in (1..100) %} {% assign foo = 1 %} {% endfor %}") t.render!(context) - assert context.resource_limits[:assign_score_current] > 0 - assert context.resource_limits[:render_score_current] > 0 - assert context.resource_limits[:render_length_current] > 0 - refute Template.default_resource_limits.key?(:assign_score_current) - refute Template.default_resource_limits.key?(:render_score_current) - refute Template.default_resource_limits.key?(:render_length_current) + assert context.resource_limits.assign_score > 0 + assert context.resource_limits.render_score > 0 + assert context.resource_limits.render_length > 0 end def test_can_use_drop_as_context