diff --git a/lib/liquid.rb b/lib/liquid.rb index af38cd42..8e5af209 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 5e8a9658..1d2127ac 100644 --- a/lib/liquid/block_body.rb +++ b/lib/liquid/block_body.rb @@ -69,8 +69,8 @@ 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_length = 0 + context.resource_limits.render_score += @nodelist.length @nodelist.each do |token| # Break out if we have any unhanded interrupts. @@ -104,9 +104,8 @@ 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 + context.resource_limits.increment_render_length(token_output) + if context.resource_limits.reached? raise MemoryError.new("Memory limits exceeded".freeze) end token_output diff --git a/lib/liquid/context.rb b/lib/liquid/context.rb index 179a466d..3aefd345 100644 --- a/lib/liquid/context.rb +++ b/lib/liquid/context.rb @@ -21,9 +21,12 @@ 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 = if resource_limits.is_a?(ResourceLimits) + resource_limits + else + ResourceLimits.new(resource_limits || Template.default_resource_limits) + end squash_instance_assigns_with_environments @this_stack_used = false @@ -36,20 +39,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 00000000..8902846b --- /dev/null +++ b/lib/liquid/resource_limits.rb @@ -0,0 +1,40 @@ +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] + + # render_length is assigned by BlockBody + # @render_length = 0 + @render_score = 0 + @assign_score = 0 + 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 increment_render_length(obj) + @render_length += increment_for(obj) + end + + def increment_render_score(obj) + @render_score += increment_for(obj) + end + + def increment_assign_score(obj) + @assign_score += increment_for(obj) + end + + private + def increment_for(obj) + obj.instance_of?(String) || obj.instance_of?(Array) || obj.instance_of?(Hash) ? obj.length : 1 + end + end +end diff --git a/lib/liquid/tags/assign.rb b/lib/liquid/tags/assign.rb index cda38780..2db8c836 100644 --- a/lib/liquid/tags/assign.rb +++ b/lib/liquid/tags/assign.rb @@ -25,7 +25,7 @@ module Liquid def render(context) val = @from.render(context) context.scopes.last[@to] = val - context.increment_used_resources(:assign_score_current, val) + context.resource_limits.increment_assign_score(val) ''.freeze end diff --git a/lib/liquid/tags/capture.rb b/lib/liquid/tags/capture.rb index 3ec0d676..4df902f7 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.increment_assign_score(output) ''.freeze end diff --git a/lib/liquid/template.rb b/lib/liquid/template.rb index 88625647..277c0260 100644 --- a/lib/liquid/template.rb +++ b/lib/liquid/template.rb @@ -18,7 +18,13 @@ module Liquid :locale => I18n.new } - attr_accessor :root, :resource_limits + attr_accessor :root + attr_reader :resource_limits + + def resource_limits=(limits) + @resource_limits = ResourceLimits.new(limits) + end + @@file_system = BlankFileSystem.new class TagRegistry @@ -110,7 +116,7 @@ module Liquid end def initialize - @resource_limits = self.class.default_resource_limits.dup + self.resource_limits = self.class.default_resource_limits end # Parse source code. diff --git a/test/integration/template_test.rb b/test/integration/template_test.rb index e3952060..4857b6ea 100644 --- a/test/integration/template_test.rb +++ b/test/integration/template_test.rb @@ -101,61 +101,62 @@ class TemplateTest < Minitest::Test t = Template.parse("0123456789") t.resource_limits = { :render_length_limit => 5 } assert_equal "Liquid error: Memory limits exceeded", t.render() - assert t.resource_limits[:reached] + 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 } 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 } assert_equal "Liquid error: Memory limits exceeded", t.render() - assert t.resource_limits[:reached] + 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 } assert_equal "Liquid error: Memory limits exceeded", t.render() - assert t.resource_limits[:reached] + 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 } 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_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