liquid 5.13.0 → 5.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 23eed44a196b2949d25540274ac7bf9c721f8f620c55f2e0740ff864dbf83488
4
- data.tar.gz: 561903b058863a29e6f8af0ac12fdda0c7941501b617f9445cb1aa5b7160cb97
3
+ metadata.gz: 160a429434128c743e92aa594ff0c0da4442ff24eaafec960b3d3bc877bd623d
4
+ data.tar.gz: 86b6682d426999c6f1528c3898671414f0f96ebf955498535e30171c639e380f
5
5
  SHA512:
6
- metadata.gz: b085504dfad17c6a0de0c152f5500eb6fe27efc588fad25b4e7ed40a3c4287642b92fb5e83f912c54534541d15e218d8721b7245d2819432d8b00f81d801a337
7
- data.tar.gz: 5d8fec9349651ea1fc4799604c3bbceae5373f9dab9c161029496479bc23f2d3450dab4ac0e36842ced63b1e92ee2d659d7ea937b8ee6edfc2efa5b51da23758
6
+ metadata.gz: 8f9fb1adebc5974b7f4e0eca38947335b359cb30fc6c6ae4058516573d760695c5ab8ef2cbf1f4cc2226d23d8a27126faf407b5da7ccc244a0b976491f0a3328
7
+ data.tar.gz: d7d200ba0a5b62d4e86bcf197f7f7a06ed2b448c25e73e196787164892b517b24d3fc4fa3f6296d03945f06755d8004590b8dc1bd0df13743ba4f8ba805e2939
data/History.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Liquid Change Log
2
2
 
3
+ ## 5.14.0
4
+
5
+ * Avoid materializing integer ranges in `for` and `tablerow` loops, and account for each visited range item [Ian Ker-Seymer]
6
+
3
7
  ## 5.13.0
4
8
 
5
9
  * Add TruffleRuby in CI [Benoit Daloze]
data/README.md CHANGED
@@ -149,6 +149,13 @@ template.render!({ 'x' => 1}, { strict_variables: true })
149
149
  #=> Liquid::UndefinedVariable: Liquid error: undefined variable y
150
150
  ```
151
151
 
152
+ ### Resource limits
153
+
154
+ `render_score_limit` and `cumulative_render_score_limit` account for each item visited by
155
+ integer-range `for` and `tablerow` loops, including loops with empty bodies. This bounds range
156
+ iteration work when a score limit is configured; `render_length_limit` only bounds generated
157
+ output and does not by itself limit CPU work for output-free loops.
158
+
152
159
  ### Usage tracking
153
160
 
154
161
  To help track usages of a feature or code path in production, we have released opt-in usage tracking. To enable this, we provide an empty `Liquid:: Usage.increment` method which you can customize to your needs. The feature is well suited to https://github.com/Shopify/statsd-instrument. However, the choice of implementation is up to you.
@@ -99,7 +99,9 @@ module Liquid
99
99
  context.handle_error(exc, line_number)
100
100
  else
101
101
  error_message = context.handle_error(exc, line_number)
102
- unless blank_tag # conditional for backwards compatibility
102
+ error_mode = context.registers.static[:template_error_mode]
103
+ suppress_error_text = blank_tag && error_mode != :strict2 && error_mode != :rigid
104
+ unless suppress_error_text # blank-tag suppression is kept for backwards compatibility outside strict2
103
105
  output << error_message
104
106
  end
105
107
  end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Liquid
4
+ class RangeSlice
5
+ attr_reader :length
6
+
7
+ def initialize(range, from, to, resource_limits)
8
+ range_length = range.end - range.begin
9
+ range_length += 1 unless range.exclude_end?
10
+ range_length = 0 if range_length.negative?
11
+
12
+ start = [from, 0].max
13
+ finish = [to || range_length, range_length].min
14
+
15
+ @first = range.begin + start
16
+ @length = [finish - start, 0].max
17
+ @direction = 1
18
+ @resource_limits = resource_limits
19
+ end
20
+
21
+ def empty?
22
+ @length.zero?
23
+ end
24
+
25
+ def each
26
+ return enum_for(:each) unless block_given?
27
+
28
+ value = @first
29
+ @length.times do
30
+ @resource_limits.increment_render_score(1)
31
+ yield value
32
+ value += @direction
33
+ end
34
+ end
35
+
36
+ def reverse!
37
+ unless empty?
38
+ @first += @direction * (@length - 1)
39
+ @direction = -@direction
40
+ end
41
+ self
42
+ end
43
+ end
44
+ end
@@ -130,7 +130,6 @@ module Liquid
130
130
  end
131
131
 
132
132
  collection = context.evaluate(@collection_name)
133
- collection = collection.to_a if collection.is_a?(Range)
134
133
 
135
134
  limit_value = context.evaluate(@limit)
136
135
  to = if limit_value.nil?
@@ -139,7 +138,9 @@ module Liquid
139
138
  Utils.to_integer(limit_value) + from
140
139
  end
141
140
 
142
- segment = Utils.slice_collection(collection, from, to)
141
+ segment = Utils.slice_collection_for_iteration(
142
+ collection, from, to, context.resource_limits, use_range_to_a: true
143
+ )
143
144
  segment.reverse! if @reversed
144
145
 
145
146
  offsets[@name] = from + segment.length
@@ -85,12 +85,13 @@ module Liquid
85
85
  from = @attributes.key?('offset') ? to_integer(context.evaluate(@attributes['offset'])) : 0
86
86
  to = @attributes.key?('limit') ? from + to_integer(context.evaluate(@attributes['limit'])) : nil
87
87
 
88
- collection = Utils.slice_collection(collection, from, to)
88
+ collection = Utils.slice_collection_for_iteration(collection, from, to, context.resource_limits, allow_endless: true)
89
89
  length = collection.length
90
90
 
91
91
  cols = @attributes.key?('cols') ? to_integer(context.evaluate(@attributes['cols'])) : length
92
92
 
93
93
  output << "<tr class=\"row1\">\n"
94
+ context.resource_limits.increment_write_score(output)
94
95
  context.stack do
95
96
  tablerowloop = Liquid::TablerowloopDrop.new(length, cols)
96
97
  context['tablerowloop'] = tablerowloop
@@ -101,6 +102,7 @@ module Liquid
101
102
  output << "<td class=\"col#{tablerowloop.col}\">"
102
103
  super
103
104
  output << '</td>'
105
+ context.resource_limits.increment_write_score(output)
104
106
 
105
107
  # Handle any interrupts if they exist.
106
108
  if context.interrupt?
@@ -110,6 +112,7 @@ module Liquid
110
112
 
111
113
  if tablerowloop.col_last && !tablerowloop.last
112
114
  output << "</tr>\n<tr class=\"row#{tablerowloop.row + 1}\">"
115
+ context.resource_limits.increment_write_score(output)
113
116
  end
114
117
 
115
118
  tablerowloop.send(:increment!)
@@ -117,6 +120,7 @@ module Liquid
117
120
  end
118
121
 
119
122
  output << "</tr>\n"
123
+ context.resource_limits.increment_write_score(output)
120
124
  output
121
125
  end
122
126
 
@@ -189,12 +189,20 @@ module Liquid
189
189
 
190
190
  context.template_name ||= name
191
191
 
192
+ previous_error_mode = context.registers.static[:template_error_mode]
193
+ context.registers.static[:template_error_mode] = @error_mode
194
+
192
195
  begin
193
196
  # render the nodelist.
194
197
  @root.render_to_output_buffer(context, output || +'')
195
198
  rescue Liquid::MemoryError => e
196
199
  context.handle_error(e)
197
200
  ensure
201
+ if previous_error_mode
202
+ context.registers.static[:template_error_mode] = previous_error_mode
203
+ else
204
+ context.registers.static.delete(:template_error_mode)
205
+ end
198
206
  @errors = context.errors
199
207
  end
200
208
  end
@@ -226,6 +234,7 @@ module Liquid
226
234
  end
227
235
 
228
236
  @warnings = parse_context.warnings
237
+ @error_mode = parse_context.error_mode
229
238
  parse_context
230
239
  end
231
240
 
data/lib/liquid/utils.rb CHANGED
@@ -13,6 +13,26 @@ module Liquid
13
13
  end
14
14
  end
15
15
 
16
+ # This is intentionally separate from slice_collection, whose Array-returning
17
+ # behavior is used outside of the iteration tags.
18
+ def self.slice_collection_for_iteration(
19
+ collection, from, to, resource_limits, allow_endless: false, use_range_to_a: false
20
+ )
21
+ if integer_range?(collection)
22
+ RangeSlice.new(collection, from, to, resource_limits)
23
+ elsif collection.is_a?(Range)
24
+ if use_range_to_a && range_method_overridden?(collection, :to_a)
25
+ # For historically honored custom Range#to_a. Charge the resulting
26
+ # selection before buffering it, just as for a custom #each.
27
+ slice_collection_for_iteration_using_each(collection.to_a, from, to, resource_limits)
28
+ else
29
+ slice_range_using_each(collection, from, to, resource_limits, allow_endless: allow_endless)
30
+ end
31
+ else
32
+ slice_collection(collection, from, to)
33
+ end
34
+ end
35
+
16
36
  def self.slice_collection_using_each(collection, from, to)
17
37
  segments = []
18
38
  index = 0
@@ -38,6 +58,55 @@ module Liquid
38
58
  segments
39
59
  end
40
60
 
61
+ # Arithmetic slicing must not bypass a Range subclass's custom #each.
62
+ def self.integer_range?(collection)
63
+ collection.instance_of?(Range) && collection.begin.is_a?(Integer) && collection.end.is_a?(Integer)
64
+ end
65
+ private_class_method :integer_range?
66
+
67
+ # Preserve support for Ruby-supplied string ranges and custom Range#each.
68
+ # Their selected length cannot be inferred from integer bounds, but the tags
69
+ # need it before rendering for loop metadata, continuation offsets, and columns.
70
+ # Buffer the selection so we do not have to replay a potentially custom iterator.
71
+ def self.slice_range_using_each(collection, from, to, resource_limits, allow_endless:)
72
+ # TableRow historically accepted an endless subclass when its custom #each
73
+ # was finite, while For historically raised through Range#to_a.
74
+ if collection.end.nil? && !(allow_endless && (!to.nil? || range_method_overridden?(collection, :each)))
75
+ raise RangeError, "cannot convert endless range to an array"
76
+ end
77
+ if collection.begin.nil? && !range_method_overridden?(collection, :each)
78
+ raise TypeError, "can't iterate from NilClass"
79
+ end
80
+
81
+ slice_collection_for_iteration_using_each(collection, from, to, resource_limits)
82
+ end
83
+ private_class_method :slice_range_using_each
84
+
85
+ # Custom Range#each can make a nominally beginless range finite; standard
86
+ # beginless ranges were rejected before reaching this budgeted traversal.
87
+ def self.slice_collection_for_iteration_using_each(collection, from, to, resource_limits)
88
+ return [] if to && to <= from
89
+
90
+ segments = []
91
+ index = 0
92
+ collection.each do |item|
93
+ break if to && to <= index
94
+
95
+ # Charge preparation, including skipped offsets, before buffering; checking
96
+ # only while rendering the buffered values would leave this work unbudgeted.
97
+ resource_limits.increment_render_score(1)
98
+ segments << item if from <= index
99
+ index += 1
100
+ end
101
+ segments
102
+ end
103
+ private_class_method :slice_collection_for_iteration_using_each
104
+
105
+ def self.range_method_overridden?(collection, method_name)
106
+ collection.method(method_name).owner != Range.instance_method(method_name).owner
107
+ end
108
+ private_class_method :range_method_overridden?
109
+
41
110
  def self.to_integer(num)
42
111
  return num if num.is_a?(Integer)
43
112
  num = num.to_s
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Liquid
5
- VERSION = "5.13.0"
5
+ VERSION = "5.14.0"
6
6
  end
data/lib/liquid.rb CHANGED
@@ -83,6 +83,7 @@ require 'liquid/resource_limits'
83
83
  require 'liquid/expression'
84
84
  require 'liquid/template'
85
85
  require 'liquid/condition'
86
+ require 'liquid/range_slice'
86
87
  require 'liquid/utils'
87
88
  require 'liquid/tokenizer'
88
89
  require 'liquid/parse_context'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: liquid
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.13.0
4
+ version: 5.14.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tobias Lütke
@@ -103,6 +103,7 @@ files:
103
103
  - lib/liquid/profiler.rb
104
104
  - lib/liquid/profiler/hooks.rb
105
105
  - lib/liquid/range_lookup.rb
106
+ - lib/liquid/range_slice.rb
106
107
  - lib/liquid/registers.rb
107
108
  - lib/liquid/resource_limits.rb
108
109
  - lib/liquid/self_drop.rb
@@ -160,7 +161,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
160
161
  - !ruby/object:Gem::Version
161
162
  version: 1.3.7
162
163
  requirements: []
163
- rubygems_version: 4.0.14
164
+ rubygems_version: 4.0.21
164
165
  specification_version: 4
165
166
  summary: A secure, non-evaling end user template engine with aesthetic markup.
166
167
  test_files: []