curlybars 1.16.1.pre.2 → 1.16.1

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: a8897a8516ee71758de87ec45acd0dcf0f177ef5e69470dcbc4e81004f57fb26
4
- data.tar.gz: 5948e415e943ff80c52fa5a756259bdda63b8943c10b77eda9bae650e384fe14
3
+ metadata.gz: 746a0625058bd9a2bca9ebf655c36f7cbd7e11e43000a051f4a76516ce73150b
4
+ data.tar.gz: f631902fba5a77cce57a36472636b99a5c03450dfbe5943e058c92b7c09e1d07
5
5
  SHA512:
6
- metadata.gz: 41f73fbddd332bb54078bd45f74dd9eb93192d8707d943d9f72af8ca29da186ad85935bd5b113ddbe74b1f3084a15431c5a13b1145b57cfd1c2b3215624733b2
7
- data.tar.gz: 3cee5f68d7ad53d3b948395e381f83465da70d7ec74a1cdc7bba37f8598ad212264698e8e675aff6925fb573710ab90cae58fb6e5d608570814c80c61b673fff
6
+ metadata.gz: c0fb5fb5398bf4643f679fdb07403de92892ee84efb70bae6a689297251e1ff9e79311b1c7711a6ba6a247a84eac945d735f72deacd8a555c9cd8ebfd75e9d81
7
+ data.tar.gz: ac273a2a866a2634c6d17ebf4a4bfd32b656c765e896214e2f3d6d2d49069232503e96bdc04a23b698d8f569cf5d368f5ab736699e280e404b9765bb370b4132
@@ -178,4 +178,6 @@ def main
178
178
  end
179
179
  end
180
180
 
181
+ # simplecov:disable
181
182
  main if __FILE__ == $PROGRAM_NAME
183
+ # simplecov:enable
@@ -13,6 +13,7 @@ module Curlybars
13
13
 
14
14
  def self.reset
15
15
  @configuration = Configuration.new
16
+ @global_helpers_dependency_tree = nil
16
17
  end
17
18
 
18
19
  class Configuration
@@ -47,8 +47,8 @@ module Curlybars
47
47
 
48
48
  contextual_block&.call(context, schema_adder)
49
49
 
50
- schema = all_methods_without_type.each_with_object({}) do |method, memo|
51
- memo[method] = nil
50
+ schema = all_methods_without_type.to_h do |method|
51
+ [method, nil]
52
52
  end
53
53
 
54
54
  methods_with_type_resolved = all_methods_with_type.transform_values do |type|
@@ -104,12 +104,19 @@ module Curlybars
104
104
  define_method(:as_json) do |*args|
105
105
  return @__as_json if defined?(@__as_json)
106
106
 
107
- @__as_json ||= allowed_methods.each_with_object({}) do |method, hash|
107
+ guard = args.first.is_a?(Hash) ? args.first[:curlybars_guard] : nil
108
+ guard&.enter!
109
+
110
+ @__as_json = allowed_methods.each_with_object({}) do |method, hash|
108
111
  unless self.method(method).arity > 0
109
112
  value = send(method)
110
- hash[method] = value.equal?(self) ? "[circular reference]" : value.as_json
113
+ serialized = value.equal?(self) ? "[circular reference]" : value.as_json(*args)
114
+ guard&.measure(serialized)
115
+ hash[method] = serialized
111
116
  end
112
117
  end
118
+ ensure
119
+ guard&.leave!
113
120
  end
114
121
  end
115
122
 
@@ -12,6 +12,7 @@ module Curlybars
12
12
  template_cache_key = '#{each_template.cache_key}'
13
13
 
14
14
  collection = rendering.coerce_to_hash!(collection, #{collection_path.path.inspect}, position)
15
+ rendering.check_nesting_not_too_deep(position)
15
16
  collection.each.with_index.map do |key_and_presenter, index|
16
17
  rendering.check_timeout!
17
18
  rendering.optional_presenter_cache(key_and_presenter[1], template_cache_key, buffer) do |buffer|
@@ -168,11 +168,6 @@ module Curlybars
168
168
 
169
169
  message = "`#{path}` cannot resolve to a helper"
170
170
  raise Curlybars::Error::Validate.new('not_a_helper', message, position)
171
- when :not_helper
172
- return unless helper?(branches)
173
-
174
- message = "`#{path}` resolves to a helper"
175
- raise Curlybars::Error::Validate.new('is_a_helper', message, position)
176
171
  when :anything
177
172
  else
178
173
  raise "invalid type `#{check_type}`"
@@ -63,22 +63,19 @@ module Curlybars
63
63
 
64
64
  type = node.resolve_and_check!(branches, check_type: check_type)
65
65
 
66
- if helper?(type)
67
- if generic_helper?(type)
68
- is_collection = type.last.is_a?(Array)
69
- return infer_generic_helper_type!(branches, is_collection: is_collection)
70
- end
71
-
72
- return type.last
66
+ if generic_helper?(type)
67
+ is_collection = type.last.is_a?(Array)
68
+ infer_generic_helper_type!(branches, is_collection: is_collection)
69
+ elsif helper?(type)
70
+ type.last
71
+ else
72
+ type
73
73
  end
74
-
75
- type
76
74
  end
77
75
 
78
76
  def generic_helper?(type)
79
- return false unless type.is_a?(Array)
77
+ return false unless helper?(type)
80
78
  return false unless type.size == 2
81
- return false unless type.first == :helper
82
79
 
83
80
  type.last == [{}] || type.last == {}
84
81
  end
@@ -9,11 +9,6 @@ module Curlybars
9
9
  <<-RUBY
10
10
  ::Module.new do
11
11
  def self.exec(contexts, rendering, variables, buffer)
12
- unless contexts.length < ::Curlybars.configuration.nesting_limit
13
- message = "Nesting too deep"
14
- position = rendering.position(#{position.line_number}, #{position.line_offset})
15
- raise ::Curlybars::Error::Render.new('nesting_too_deep', message, position)
16
- end
17
12
  #{compiled_items}
18
13
  end
19
14
  end.exec(contexts, rendering, variables, buffer)
@@ -10,6 +10,7 @@ module Curlybars
10
10
  if rendering.to_bool(compiled_path)
11
11
  position = rendering.position(#{position.line_number}, #{position.line_offset})
12
12
  rendering.check_context_is_presenter(compiled_path, #{presenter_path.path.inspect}, position)
13
+ rendering.check_nesting_not_too_deep(position)
13
14
 
14
15
  contexts.push(compiled_path)
15
16
  begin
@@ -82,8 +82,7 @@ module Curlybars
82
82
  stack.pop unless stack.empty?
83
83
  end
84
84
 
85
- remaining_segments = full_path.empty? ? [] : full_path.split('.')
86
- stack + remaining_segments
85
+ stack + full_path.split('.')
87
86
  end
88
87
 
89
88
  def handle_block_helper(node, context_stack)
@@ -31,6 +31,16 @@ module Curlybars
31
31
  raise ::Curlybars::Error::Render.new('timeout', message, nil)
32
32
  end
33
33
 
34
+ # Called before a #with or #each block pushes its context. The contexts
35
+ # stack starts with the root presenter, so its length is one more than
36
+ # the number of enclosing #with and #each blocks.
37
+ def check_nesting_not_too_deep(position)
38
+ return if contexts.length <= Curlybars.configuration.nesting_limit
39
+
40
+ message = "Nesting too deep (> #{Curlybars.configuration.nesting_limit} levels)"
41
+ raise Curlybars::Error::Render.new('nesting_too_deep', message, position)
42
+ end
43
+
34
44
  def check_context_is_presenter(context, path, position)
35
45
  return if presenter?(context)
36
46
 
@@ -205,7 +215,7 @@ module Curlybars
205
215
 
206
216
  eval(eval_source) # rubocop:disable Security/Eval -- eval is the established compilation pattern for the whole engine
207
217
  rescue Curlybars::Error::Render => e
208
- raise if e.id == 'render.timeout' || e.id == 'render.output_too_long'
218
+ raise if e.id == 'render.timeout' || e.id == 'render.output_too_long' || e.id == 'render.nesting_too_deep'
209
219
 
210
220
  ""
211
221
  rescue StandardError
@@ -257,8 +267,11 @@ module Curlybars
257
267
  raise Curlybars::Error::Render.new('unallowed_path', message, position)
258
268
  end
259
269
 
270
+ # Counts the dots after the final `/` in the path. Leading `../` segments
271
+ # move up the contexts stack rather than traversing, so they don't count.
260
272
  def check_traverse_not_too_deep(traverse, position)
261
- return unless traverse.count('.') > Curlybars.configuration.traversing_limit
273
+ dots = traverse.split('/').last.count('.')
274
+ return unless dots > Curlybars.configuration.traversing_limit
262
275
 
263
276
  message = "`#{traverse}` too deep"
264
277
  raise Curlybars::Error::Render.new('traverse_too_deep', message, position)
@@ -0,0 +1,35 @@
1
+ module Curlybars
2
+ class SerializationGuard
3
+ def initialize(budget:, depth_limit:)
4
+ @budget = budget
5
+ @depth_limit = depth_limit
6
+ @accumulated = 0
7
+ @depth = 0
8
+ end
9
+
10
+ def enter!
11
+ @depth += 1
12
+ return unless @depth > @depth_limit
13
+
14
+ message = "Serialization nesting too deep (> #{@depth_limit})"
15
+ raise Curlybars::Error::Render.new('nesting_too_deep', message, nil)
16
+ end
17
+
18
+ def leave!
19
+ @depth -= 1
20
+ end
21
+
22
+ def measure(value)
23
+ # Only String leaves are counted: deep-measuring every Hash/Array would
24
+ # mean serializing twice. This under-counts raw hash/array fields; the
25
+ # authoritative ceiling is still SafeBuffer#concat at the output node.
26
+ return unless value.is_a?(String)
27
+
28
+ @accumulated += value.bytesize
29
+ return unless @accumulated > @budget
30
+
31
+ message = "Output too long (> #{@budget} bytes)"
32
+ raise Curlybars::Error::Render.new('output_too_long', message, nil)
33
+ end
34
+ end
35
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Curlybars
4
- VERSION = '1.16.1.pre.2'
4
+ VERSION = '1.16.1'
5
5
  end
data/lib/curlybars.rb CHANGED
@@ -29,7 +29,7 @@ module Curlybars
29
29
  cache_key = ["Curlybars.compile", Curlybars::VERSION, identifier, Digest::SHA256.hexdigest(source)]
30
30
 
31
31
  cache.fetch(cache_key) do
32
- ast(transformed_source(source), identifier, run_processors: true).compile
32
+ ast(transformed_source(source, identifier), identifier, run_processors: true).compile
33
33
  end
34
34
  end
35
35
 
@@ -76,7 +76,7 @@ module Curlybars
76
76
  # source - The source HBS String used to generate an AST.
77
77
  # identifier - The the file name of the template being checked (defaults to `nil`).
78
78
  def visit(visitor, source, identifier = nil)
79
- tree = ast(transformed_source(source), identifier, run_processors: true)
79
+ tree = ast(transformed_source(source, identifier), identifier, run_processors: true)
80
80
  visitor.accept(tree)
81
81
  end
82
82
 
@@ -99,11 +99,19 @@ module Curlybars
99
99
  #
100
100
  # Returns an Array of Curlybars::Node::Path instances that match the target path.
101
101
  def find(target_path, source, identifier = nil, role: nil)
102
- tree = ast(transformed_source(source), identifier, run_processors: true)
102
+ tree = ast(transformed_source(source, identifier), identifier, run_processors: true)
103
103
  finder = Curlybars::PathFinder.new(tree)
104
104
  finder.find(target_path, role: role)
105
105
  end
106
106
 
107
+ def serialize(value)
108
+ guard = Curlybars::SerializationGuard.new(
109
+ budget: configuration.output_limit,
110
+ depth_limit: configuration.nesting_limit
111
+ )
112
+ value.to_json(curlybars_guard: guard)
113
+ end
114
+
107
115
  def global_helpers_dependency_tree
108
116
  @global_helpers_dependency_tree ||= begin
109
117
  classes = Curlybars.configuration.global_helpers_provider_classes
@@ -119,7 +127,7 @@ module Curlybars
119
127
 
120
128
  private
121
129
 
122
- def transformed_source(source)
130
+ def transformed_source(source, identifier)
123
131
  transformers = Curlybars.configuration.compiler_transformers
124
132
  transformers.inject(source) do |memo, transformer|
125
133
  transformer.transform(memo, identifier)
@@ -147,6 +155,7 @@ module Curlybars
147
155
  end
148
156
 
149
157
  require 'curlybars/safe_buffer'
158
+ require 'curlybars/serialization_guard'
150
159
  require 'curlybars/configuration'
151
160
  require 'curlybars/rendering_support'
152
161
  require 'curlybars/parser'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: curlybars
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.16.1.pre.2
4
+ version: 1.16.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Libo Cannici
@@ -96,7 +96,6 @@ files:
96
96
  - lib/curlybars/lexer.rb
97
97
  - lib/curlybars/method_whitelist.rb
98
98
  - lib/curlybars/node/block_helper_else.rb
99
- - lib/curlybars/node/boolean.rb
100
99
  - lib/curlybars/node/each_else.rb
101
100
  - lib/curlybars/node/if_else.rb
102
101
  - lib/curlybars/node/item.rb
@@ -106,7 +105,6 @@ files:
106
105
  - lib/curlybars/node/partial.rb
107
106
  - lib/curlybars/node/path.rb
108
107
  - lib/curlybars/node/root.rb
109
- - lib/curlybars/node/string.rb
110
108
  - lib/curlybars/node/sub_expression.rb
111
109
  - lib/curlybars/node/template.rb
112
110
  - lib/curlybars/node/text.rb
@@ -123,6 +121,7 @@ files:
123
121
  - lib/curlybars/railtie.rb
124
122
  - lib/curlybars/rendering_support.rb
125
123
  - lib/curlybars/safe_buffer.rb
124
+ - lib/curlybars/serialization_guard.rb
126
125
  - lib/curlybars/template_handler.rb
127
126
  - lib/curlybars/validation_context.rb
128
127
  - lib/curlybars/version.rb
@@ -139,7 +138,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
139
138
  requirements:
140
139
  - - ">="
141
140
  - !ruby/object:Gem::Version
142
- version: '3.2'
141
+ version: '3.3'
143
142
  required_rubygems_version: !ruby/object:Gem::Requirement
144
143
  requirements:
145
144
  - - ">="
@@ -1,24 +0,0 @@
1
- module Curlybars
2
- module Node
3
- Boolean = Struct.new(:boolean) do
4
- def compile
5
- # NOTE: the following is a heredoc string, representing the ruby code fragment
6
- # outputted by this node.
7
- <<-RUBY
8
- ->() { #{boolean} }
9
- RUBY
10
- end
11
-
12
- def validate(branches, context: nil)
13
- # Nothing to validate here.
14
- end
15
-
16
- def cache_key
17
- [
18
- boolean.to_s,
19
- self.class.name
20
- ].join("/")
21
- end
22
- end
23
- end
24
- end
@@ -1,24 +0,0 @@
1
- module Curlybars
2
- module Node
3
- String = Struct.new(:string) do
4
- def compile
5
- # NOTE: the following is a heredoc string, representing the ruby code fragment
6
- # outputted by this node.
7
- <<-RUBY
8
- ->() { #{string.inspect} }
9
- RUBY
10
- end
11
-
12
- def validate(branches, context: nil)
13
- # Nothing to validate here.
14
- end
15
-
16
- def cache_key
17
- [
18
- string,
19
- self.class.name
20
- ].join("/")
21
- end
22
- end
23
- end
24
- end