safemode 1.5.0 → 2.0.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: 795ca593ca92570d9d4a54cbad72c18c32574d1190b37d002acb22f0347814e0
4
- data.tar.gz: 585a9de965e98cc5f7d847357db1db8ce2a52e1200cf17169dc109c4cda231b5
3
+ metadata.gz: 31ed45049398953b35fafe5ff837df2332712caa9534c5278e971d9031a61836
4
+ data.tar.gz: 49b507273a0ef6d03578be21a26f26388fb4cd54d253670a8932ae7891ae40f7
5
5
  SHA512:
6
- metadata.gz: d27b2e9fa7e9893cc43039c37c884e21c1e7945974d3614a907818453e99b88d0cd37ab68d43058e01303f3d6bc6966f51edeee009a712994ad40b754660a4d7
7
- data.tar.gz: 1b6ddc2ab11f82cc6275d9ea26aa57800ebcfc2a2e7a84612894bb61b17a879e1b83eac20dff722091fab596eeb04912110d4bb70dbcce924771b189c218894f
6
+ metadata.gz: eefb3d5121ad93910e1468cd635a5d77320dfbd23c341469af7acc1d90c80947ab1f8ec058bf1775b9269566d2503cfec0118bb6143e9d35ee6ba5afbb055caa
7
+ data.tar.gz: ab5f0385d055999f6a6463381d24494bab377fe12aebec639b3be5fc78faf4355c5485105ba6c73bbc0a0023c2db3dd063bc35abee19d4564748d15a925650d8
data/demo.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'safemode'
2
4
  require 'erb'
3
5
 
@@ -17,15 +17,11 @@ module Safemode
17
17
  end
18
18
 
19
19
  def core_classes
20
- klasses = [ Array, Float, Hash, Range, String, Symbol, Time, NilClass, FalseClass, TrueClass ]
20
+ klasses = [ Array, Float, Hash, Integer, Range, String, Symbol, Time, NilClass, FalseClass, TrueClass ]
21
21
  klasses << Date if defined? Date
22
22
  klasses << DateTime if defined? DateTime
23
- if RUBY_VERSION >= '2.4.0'
24
- klasses << Integer
25
- else
26
- klasses << Bignum
27
- klasses << Fixnum
28
- end
23
+ klasses << Data if defined? Data # Ruby 3.2 addition
24
+ klasses << Set if defined? Set # Ruby 3.2 addition
29
25
  klasses
30
26
  end
31
27
 
@@ -46,31 +42,21 @@ module Safemode
46
42
  # whitelisted methods for core classes ... kind of arbitrary selection
47
43
  @@methods_whitelist = {
48
44
  'Array' => %w(any? assoc at blank? collect collect! compact compact!
49
- concat delete delete_at delete_if each each_index empty?
50
- fetch fill first flatten flatten! hash include? index
45
+ concat delete delete_at delete_if each each_index each_with_index
46
+ empty? fetch fill first flatten flatten! hash include? index
51
47
  indexes indices inject insert join last length map map! max min
52
48
  nitems pop push present? rassoc reject reject! reverse
53
49
  reverse! reverse_each rindex select shift size slice
54
50
  slice! sort sort! transpose to_sentence uniq uniq! unshift
55
51
  values_at zip),
56
52
 
57
- 'Bignum' => %w(abs blank? ceil chr coerce div divmod downto floor hash
58
- integer? modulo next nonzero? present? quo remainder round
59
- singleton_method_added size step succ times to_f to_i
60
- to_int to_s truncate upto zero?),
61
-
62
- 'Fixnum' => %w(abs blank? ceil chr coerce div divmod downto floor id2name
63
- integer? modulo modulo next nonzero? present? quo remainder
64
- round singleton_method_added size step succ times to_f to_i
65
- to_int to_s to_sym truncate upto zero?),
66
-
67
53
  'Float' => %w(abs blank? ceil coerce div divmod finite? floor hash
68
54
  infinite? integer? modulo nan? nonzero? present? quo
69
55
  remainder round singleton_method_added step to_f to_i
70
56
  to_int to_s truncate zero?),
71
57
 
72
- 'Hash' => %w(any? blank? clear delete delete_if each each_key
73
- each_pair each_value empty? fetch dig has_key? has_value?
58
+ 'Hash' => %w(any? blank? clear delete delete_if each each_key each_pair
59
+ each_value each_with_index empty? fetch dig has_key? has_value?
74
60
  include? index invert key? keys length member? merge merge!
75
61
  present? rec_merge! rehash reject reject! select shift
76
62
  size sort store update value? values values_at),
@@ -116,6 +102,17 @@ module Safemode
116
102
  'DateTime' => %w(blank? hour min new_offset newof of offset present? sec
117
103
  sec_fraction strftime to_datetime_default_s to_json zone),
118
104
 
105
+ 'Data' => %w(deconstruct deconstruct_keys hash members),
106
+
107
+ 'Set' => %w(any? blank? clear clone collect count delete delete?
108
+ delete_if difference disjoint? each each_with_index
109
+ each_with_object empty? filter find first flatten
110
+ flatten! hash include? inject intersect? intersection
111
+ join keep_if length map max member? merge min
112
+ present? reject reject! replace select select!
113
+ size sort sort_by subset? superset? union
114
+ uniq zip),
115
+
119
116
  'NilClass' => %w(blank? duplicable? present? to_f to_i),
120
117
 
121
118
  'FalseClass' => %w(blank? duplicable? present?),
@@ -1,8 +1,5 @@
1
1
  module Safemode
2
2
  class Parser < Ruby2Ruby
3
- # @@parser = defined?(RubyParser) ? 'RubyParser' : 'ParseTree'
4
- @@parser = 'RubyParser'
5
-
6
3
  class << self
7
4
  def jail(code, allowed_fcalls = [])
8
5
  @@allowed_fcalls = allowed_fcalls
@@ -11,18 +8,7 @@ module Safemode
11
8
  end
12
9
 
13
10
  def parse(code)
14
- case @@parser
15
- # when 'ParseTree'
16
- # ParseTree.translate(code)
17
- when 'RubyParser'
18
- RubyParser.new.parse(code)
19
- else
20
- raise "unknown parser #{@@parser}"
21
- end
22
- end
23
-
24
- def parser=(parser)
25
- @@parser = parser
11
+ Prism::Translation::RubyParser.parse(code)
26
12
  end
27
13
  end
28
14
 
@@ -89,8 +75,13 @@ module Safemode
89
75
  # :colon2 is used for module constants
90
76
  :colon2,
91
77
  # unnecessarily advanced?
92
- :argscat, :argspush, :splat,
93
- :op_asgn1, :op_asgn2, :op_asgn_and, :op_asgn_or,
78
+ :argscat, :argspush, :splat, :kwsplat,
79
+ # NOTE: op_asgn*/safe_op_asgn* do not insert .to_jail on the
80
+ # receiver. Setters are called directly on the real object, bypassing Jail.
81
+ :op_asgn, :op_asgn1, :op_asgn2, :op_asgn_and, :op_asgn_or,
82
+ :safe_op_asgn, :safe_op_asgn2,
83
+ # pattern matching (Ruby 3.0+)
84
+ :in, :array_pat, :hash_pat, :find_pat, :kwrest,
94
85
  # needed for haml
95
86
  :block ]
96
87
 
@@ -98,18 +89,21 @@ module Safemode
98
89
  # see below for :const handling
99
90
  :defn, :defs, :alias, :valias, :undef, :class, :attrset,
100
91
  :module, :sclass, :colon3,
101
- :fbody, :scope, :block_arg, :postexe,
92
+ :fbody, :scope, :block_arg, :postexe, :preexe,
102
93
  :redo, :retry, :begin, :rescue, :resbody, :ensure,
103
94
  :defined, :super, :zsuper, :return,
104
95
  :dmethod, :bmethod, :to_ary, :svalue, :match,
105
- :attrasgn, :cdecl, :cvasgn, :cvdecl, :cvar, :gvar, :gasgn,
96
+ :attrasgn, :safe_attrasgn,
97
+ :cdecl, :cvasgn, :cvdecl, :cvar, :gvar, :gasgn,
106
98
  :xstr, :dxstr,
107
99
  # not sure how secure ruby regexp is, so leave it out for now
108
100
  :dregx, :dregx_once, :match2, :match3, :nth_ref, :back_ref,
109
101
  # block_pass represents &:method, which would bypass the whitelist e.g. by array.each(&:destroy)
110
102
  # at this point we don't know the receiver so we rather disable it completely,
111
103
  # use array.each { |item| item.destroy } instead
112
- :block_pass ]
104
+ :block_pass,
105
+ # lambda creates Proc objects
106
+ :lambda ]
113
107
 
114
108
  # SexpProcessor bails when we overwrite these ... but they are listed as
115
109
  # "internal nodes that you can't get to" in sexp_processor.rb
@@ -207,6 +201,49 @@ module Safemode
207
201
  end
208
202
  end
209
203
 
204
+ # Ruby2Ruby bug: __var adds ^ (pin) to all lvars inside :in context,
205
+ # including the body after "then" where ^ is invalid syntax.
206
+ # Fix: process body outside the :in context.
207
+ def process_in(exp)
208
+ _, pattern, *body = exp
209
+
210
+ guard = extract_guard(pattern)
211
+ cond = process pattern
212
+ body = body.compact.map { |sexp|
213
+ in_context :in_body do
214
+ indent process sexp
215
+ end
216
+ }
217
+
218
+ body << indent("# do nothing") if body.empty?
219
+ body = body.join "\n"
220
+
221
+ header = "in #{cond}"
222
+ header << " #{guard}" if guard
223
+ "#{header} then\n#{body.chomp}"
224
+ end
225
+
226
+ # Prism translates guard clauses (in pattern if cond / in pattern unless cond)
227
+ # as s(:if, guard, pattern, nil) / s(:if, guard, nil, pattern). We detect this
228
+ # and rewrite the sexp in-place so process_if renders only the pattern, then
229
+ # return the guard string for process_in to append.
230
+ def extract_guard(pattern)
231
+ return unless pattern.sexp_type == :if
232
+
233
+ _, guard_sexp, if_body, else_body = pattern
234
+ if if_body && !else_body
235
+ guard_str = in_context(:in_body) { "if #{process guard_sexp.deep_clone}" }
236
+ pattern.clear
237
+ if_body.each { |node| pattern << node }
238
+ elsif else_body && !if_body
239
+ guard_str = in_context(:in_body) { "unless #{process guard_sexp.deep_clone}" }
240
+ pattern.clear
241
+ else_body.each { |node| pattern << node }
242
+ end
243
+
244
+ guard_str
245
+ end
246
+
210
247
  # Ruby2Ruby process_if rewrites if and unless statements in a way that
211
248
  # makes the result unusable for evaluation in, e.g. ERB which appends a
212
249
  # call to to_s when using <%= %> tags. We'd need to either enclose the
data/lib/safemode.rb CHANGED
@@ -1,17 +1,8 @@
1
1
  require 'rubygems'
2
2
 
3
3
  require 'ruby2ruby'
4
- begin
5
- require 'ruby_parser' # try to load RubyParser and use it if present
6
- rescue LoadError => e
7
- end
8
- # this doesn't work somehow. Maybe something changed inside
9
- # ParseTree or sexp_processor or so.
10
- # (the require itself works, but ParseTree doesn't play nice)
11
- # begin
12
- # require 'parse_tree'
13
- # rescue LoadError => e
14
- # end
4
+ require 'prism'
5
+ require 'prism/translation/ruby_parser'
15
6
 
16
7
  require 'safemode/core_ext'
17
8
  require 'safemode/blankslate'
data/safemode.gemspec CHANGED
@@ -4,10 +4,10 @@ require 'date'
4
4
 
5
5
  Gem::Specification.new do |s|
6
6
  s.name = "safemode".freeze
7
- s.version = "1.5.0"
7
+ s.version = "2.0.0"
8
8
  s.date = Date.today
9
9
 
10
- s.summary = "A library for safe evaluation of Ruby code based on ParseTree/RubyParser and Ruby2Ruby"
10
+ s.summary = "A library for safe evaluation of Ruby code based on Prism and Ruby2Ruby"
11
11
  s.description = "A library for safe evaluation of Ruby code based on RubyParser and Ruby2Ruby. Provides Rails ActionView template handlers for ERB and Haml."
12
12
  s.homepage = "https://github.com/svenfuchs/safemode"
13
13
  s.licenses = ["MIT"]
@@ -52,14 +52,14 @@ Gem::Specification.new do |s|
52
52
  "test/test_safemode_parser.rb"
53
53
  ]
54
54
 
55
- s.required_ruby_version = ">= 2.7", "< 3.2"
55
+ s.required_ruby_version = ">= 3.0", "< 3.4"
56
56
 
57
- s.add_runtime_dependency "ruby2ruby", ">= 2.4.0"
58
- s.add_runtime_dependency "ruby_parser", ">= 3.10.1"
59
- s.add_runtime_dependency "sexp_processor", ">= 4.10.0"
57
+ s.add_runtime_dependency "ruby2ruby", "~> 2.4"
58
+ s.add_runtime_dependency "prism", "~> 1.0"
59
+ s.add_runtime_dependency "sexp_processor", "~> 4.10"
60
60
 
61
- s.add_development_dependency "rake"
62
- s.add_development_dependency "rdoc"
63
- s.add_development_dependency "simplecov"
64
- s.add_development_dependency "test-unit"
61
+ s.add_development_dependency "rake", "~> 13.4"
62
+ s.add_development_dependency "rdoc", "~> 7.2"
63
+ s.add_development_dependency "simplecov", "~> 0.22"
64
+ s.add_development_dependency "test-unit", "~> 3.7"
65
65
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require File.join(File.dirname(__FILE__), 'test_helper')
2
4
 
3
5
  class TestERBEval < Test::Unit::TestCase
@@ -58,19 +60,17 @@ class TestERBEval < Test::Unit::TestCase
58
60
  end
59
61
 
60
62
  TestHelper.no_method_error_raising_calls.each do |call|
61
- call.gsub!('"', '\\\\"')
62
63
  class_eval %Q(
63
64
  def test_calling_#{call.gsub(/[\W]/, '_')}_should_raise_no_method
64
- assert_raise_no_method "#{call}", @assigns, @locals
65
+ assert_raise_no_method "#{call.gsub('"', '\\\\"')}", @assigns, @locals
65
66
  end
66
67
  )
67
68
  end
68
69
 
69
70
  TestHelper.security_error_raising_calls.each do |call|
70
- call.gsub!('"', '\\\\"')
71
71
  class_eval %Q(
72
72
  def test_calling_#{call.gsub(/[\W]/, '_')}_should_raise_security
73
- assert_raise_security "#{call}", @assigns, @locals
73
+ assert_raise_security "#{call.gsub('"', '\\\\"')}", @assigns, @locals
74
74
  end
75
75
  )
76
76
  end
data/test/test_helper.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  if ENV['COVERAGE']
2
4
  require 'simplecov'
3
5
  SimpleCov.start {add_filter 'test_'}
@@ -110,6 +112,8 @@ class Article
110
112
  Comment
111
113
  end
112
114
 
115
+ attr_accessor :status
116
+
113
117
  def method_with_kwargs(a_keyword: false)
114
118
  a_keyword
115
119
  end
@@ -148,7 +152,7 @@ class Comment
148
152
  end
149
153
 
150
154
  class Article::Jail < Safemode::Jail
151
- allow :title, :comments, :is_article?, :comment_class, :method_with_kwargs
155
+ allow :title, :comments, :is_article?, :comment_class, :method_with_kwargs, :status
152
156
 
153
157
  def author_name
154
158
  "this article's author name"
data/test/test_jail.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require File.join(File.dirname(__FILE__), 'test_helper')
2
4
 
3
5
  class TestJail < Test::Unit::TestCase
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require File.join(File.dirname(__FILE__), 'test_helper')
2
4
 
3
5
  class TestSafemodeEval < Test::Unit::TestCase
@@ -92,26 +94,151 @@ class TestSafemodeEval < Test::Unit::TestCase
92
94
  assert @box.eval("@article.method_with_kwargs(a_keyword: true)", @assigns)
93
95
  end
94
96
 
97
+ def test_pattern_matching_with_literal
98
+ assert_equal "matched", @box.eval('case 1; in 1; "matched"; in 2; "nope"; end')
99
+ end
100
+
101
+ def test_pattern_matching_with_array_destructuring
102
+ assert_equal 1, @box.eval('case [1, 2, 3]; in [a, b, c]; a; end')
103
+ end
104
+
105
+ def test_pattern_matching_with_hash_destructuring
106
+ assert_equal 1, @box.eval('case({a: 1, b: 2}); in {a:}; a; end')
107
+ end
108
+
109
+ def test_pattern_matching_with_assign
110
+ assert_equal "two", @box.eval('case @val; in 1; "one"; in 2; "two"; end', { val: 2 })
111
+ end
112
+
113
+ def test_pattern_matching_with_find_pattern
114
+ assert_equal "found", @box.eval('case [1, 2, 3]; in [*, 2, *]; "found"; end')
115
+ end
116
+
117
+ def test_pattern_matching_with_pin_operator
118
+ assert_equal "matched", @box.eval('y = 1; case 1; in ^y; "matched"; end')
119
+ end
120
+
121
+ def test_pattern_matching_with_multiple_clauses
122
+ assert_equal "second", @box.eval('case [3, 4]; in [1, 2]; "first"; in [3, 4]; "second"; end')
123
+ end
124
+
125
+ def test_pattern_matching_no_match_returns_nil
126
+ assert_nil @box.eval('case 3; in 1; "one"; in 2; "two"; else; nil; end')
127
+ end
128
+
129
+ def test_pattern_matching_with_if_guard
130
+ assert_equal "positive", @box.eval('case 1; in x if x > 0; "positive"; end')
131
+ end
132
+
133
+ def test_pattern_matching_with_unless_guard
134
+ assert_equal "not negative", @box.eval('case 1; in x unless x < 0; "not negative"; end')
135
+ end
136
+
137
+ def test_pattern_matching_guard_no_match
138
+ assert_nil @box.eval('case 1; in x if x < 0; "negative"; else; nil; end')
139
+ end
140
+
141
+ def test_pattern_matching_guard_with_array_pattern
142
+ assert_equal "yes", @box.eval('case [1, 2]; in [a, b] if a > 0; "yes"; end')
143
+ end
144
+
145
+ def test_pattern_matching_guard_with_hash_pattern
146
+ assert_equal "alice", @box.eval('case({name: "alice"}); in {name:} if name.start_with?("a"); name; end')
147
+ end
148
+
149
+ def test_rightward_assignment
150
+ assert_equal 1, @box.eval('[1, 2] => [a, b]; a')
151
+ end
152
+
153
+ def test_pattern_matching_with_jailed_hash
154
+ assert_equal "an article title", @box.eval('case @data; in {title:}; title; end', { data: { title: "an article title" } })
155
+ end
156
+
157
+ def test_hash_shorthand
158
+ # TODO: Remove the check once Ruby 3.1 is the minimum
159
+ if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.1')
160
+ assert_equal({ a: 1, b: 2 }, @box.eval('a = 1; b = 2; { a:, b: }'))
161
+ end
162
+ end
163
+
164
+ def test_endless_range
165
+ assert_equal [3, 4, 5], @box.eval('[1,2,3,4,5][2..]')
166
+ end
167
+
168
+ def test_beginless_range
169
+ assert_equal [1, 2, 3], @box.eval('[1,2,3,4,5][..2]')
170
+ end
171
+
172
+ # attrasgn (@article.status = val) is blocked at parse time
173
+ def test_attrasgn_is_blocked
174
+ assert_raise(Safemode::SecurityError) { @box.eval('@article.status = "new_value"', @assigns) }
175
+ end
176
+
177
+ def test_safe_attrasgn_is_blocked
178
+ assert_raise(Safemode::SecurityError) { @box.eval('@article&.status = "new_value"', @assigns) }
179
+ end
180
+
181
+ # op_asgn2 (@article.status ||= val) is NOT blocked at parse time.
182
+ # NOTE: .to_jail is not inserted on the receiver, so the setter is called
183
+ # directly on the real object, bypassing the Jail whitelist.
184
+ def test_op_asgn2_bypasses_jail
185
+ article = Article.new
186
+ assert_nil article.status
187
+ @box.eval('@article.status ||= "new_value"', { article: article })
188
+ assert_equal "new_value", article.status
189
+ end
190
+
191
+ def test_safe_op_asgn2_bypasses_jail
192
+ article = Article.new
193
+ assert_nil article.status
194
+ @box.eval('@article&.status ||= "new_value"', { article: article })
195
+ assert_equal "new_value", article.status
196
+ end
197
+
198
+ def test_lambda_is_blocked
199
+ assert_raise(Safemode::SecurityError) { @box.eval('-> { 1 }') }
200
+ end
201
+
95
202
  def test_sending_to_method_missing
96
203
  assert_raise_with_message(Safemode::NoMethodError, /#no_such_method/) do
97
204
  @box.eval("@article.no_such_method('arg', key: 'value')", @assigns)
98
205
  end
99
206
  end
100
207
 
208
+ if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.2')
209
+ def test_pattern_matching_with_data_subclass
210
+ point_class = Data.define(:x, :y)
211
+ point = point_class.new(x: 10, y: 20)
212
+ assert_equal 10, @box.eval('case @point; in {x:}; x; end', { point: point })
213
+ end
214
+
215
+ def test_data_subclass_inherits_data_jail
216
+ point_class = Data.define(:x, :y)
217
+ point = point_class.new(x: 10, y: 20)
218
+ assert_equal [:x, :y], @box.eval('@point.members', { point: point })
219
+ assert_equal [10, 20], @box.eval('@point.deconstruct', { point: point })
220
+ assert_equal({ x: 10 }, @box.eval('@point.deconstruct_keys([:x])', { point: point }))
221
+ end
222
+
223
+ def test_data_subclass_jail_blocks_non_whitelisted_methods
224
+ point_class = Data.define(:x, :y)
225
+ point = point_class.new(x: 10, y: 20)
226
+ assert_raise(Safemode::NoMethodError) { @box.eval('@point.instance_variables', { point: point }) }
227
+ end
228
+ end
229
+
101
230
  TestHelper.no_method_error_raising_calls.each do |call|
102
- call.gsub!('"', '\\\\"')
103
231
  class_eval %Q(
104
232
  def test_calling_#{call.gsub(/[\W]/, '_')}_should_raise_no_method
105
- assert_raise_no_method "#{call}", @assigns, @locals
233
+ assert_raise_no_method "#{call.gsub('"', '\\\\"')}", @assigns, @locals
106
234
  end
107
235
  )
108
236
  end
109
237
 
110
238
  TestHelper.security_error_raising_calls.each do |call|
111
- call.gsub!('"', '\\\\"')
112
239
  class_eval %Q(
113
240
  def test_calling_#{call.gsub(/[\W]/, '_')}_should_raise_security
114
- assert_raise_security "#{call}", @assigns, @locals
241
+ assert_raise_security "#{call.gsub('"', '\\\\"')}", @assigns, @locals
115
242
  end
116
243
  )
117
244
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require File.join(File.dirname(__FILE__), 'test_helper')
2
4
 
3
5
  class TestSafemodeParser < Test::Unit::TestCase
@@ -65,6 +67,107 @@ class TestSafemodeParser < Test::Unit::TestCase
65
67
  end
66
68
  end
67
69
 
70
+ def test_safe_attrasgn_is_disabled
71
+ assert_raise Safemode::SecurityError do
72
+ jail('@article&.title = "new_value"')
73
+ end
74
+ end
75
+
76
+ def test_safe_op_asgn2_is_allowed
77
+ assert_nothing_raised do
78
+ jail('@article&.title ||= "new_value"')
79
+ end
80
+ end
81
+
82
+ def test_lambda_is_disabled
83
+ assert_raise Safemode::SecurityError do
84
+ jail('-> { 1 }')
85
+ end
86
+ end
87
+
88
+ def test_case_in_with_literal
89
+ jailed = jail("case x; in 1; \"one\"; in 2; \"two\"; end")
90
+ assert_match(/in 1 then/, jailed)
91
+ assert_match(/in 2 then/, jailed)
92
+ assert_match(/to_jail\.x/, jailed)
93
+ end
94
+
95
+ def test_case_in_with_array_pattern
96
+ jailed = jail("case x; in [a, b]; a; end")
97
+ assert_match(/in \[a, b\] then/, jailed)
98
+ assert_no_match(/\^a/, jailed)
99
+ end
100
+
101
+ def test_case_in_with_hash_pattern
102
+ jailed = jail("case x; in {name:}; name; end")
103
+ assert_match(/in \{ name: \} then/, jailed)
104
+ assert_no_match(/\^name/, jailed)
105
+ end
106
+
107
+ def test_case_in_with_find_pattern
108
+ jailed = jail("case x; in [*, 2, *]; \"found\"; end")
109
+ assert_match(/in \[\*, 2, \*\] then/, jailed)
110
+ end
111
+
112
+ def test_case_in_pin_operator
113
+ jailed = jail("y = 1; case x; in ^y; true; end")
114
+ assert_match(/in \^y then/, jailed)
115
+ end
116
+
117
+ def test_case_in_body_does_not_pin_variables
118
+ jailed = jail("case x; in [a, b]; a; end")
119
+ lines = jailed.lines
120
+ body_start = lines.index { |l| l.match?(/^in \[/) } + 1
121
+ lines[body_start..].each { |line| assert_no_match(/\^[a-z]/, line) }
122
+ end
123
+
124
+ def test_case_in_multiple_clauses
125
+ jailed = jail("case x; in [a, b]; a; in {c:}; c; end")
126
+ assert_match(/in \[a, b\] then/, jailed)
127
+ assert_match(/in \{ c: \} then/, jailed)
128
+ end
129
+
130
+ def test_case_in_with_if_guard
131
+ jailed = jail("case x; in 1 if true; \"matched\"; end")
132
+ assert_match(/in 1 if true then/, jailed)
133
+ end
134
+
135
+ def test_case_in_with_unless_guard
136
+ jailed = jail("case x; in 1 unless false; \"matched\"; end")
137
+ assert_match(/in 1 unless false then/, jailed)
138
+ end
139
+
140
+ def test_case_in_guard_does_not_pin_variables
141
+ jailed = jail("case x; in [a, b] if a; a; end")
142
+ guard_line = jailed.lines.find { |l| l.match?(/^in \[/) }
143
+ assert_match(/if a then/, guard_line)
144
+ assert_no_match(/\^a/, guard_line)
145
+ end
146
+
147
+ def test_case_in_guard_jails_method_calls
148
+ jailed = jail('case x; in {name: n} if n.start_with?("a"); n; end')
149
+ assert_match(/if n\.to_jail\.start_with\?/, jailed)
150
+ end
151
+
152
+ def test_rightward_assignment
153
+ jailed = jail("x => a")
154
+ assert_match(/case to_jail\.x/, jailed)
155
+ assert_match(/in a then/, jailed)
156
+ end
157
+
158
+ def test_hash_shorthand_in_literal
159
+ jailed = jail("a = 1; b = 2; { a:, b: }")
160
+ assert_match(/a:,/, jailed)
161
+ end
162
+
163
+ def test_endless_range
164
+ assert_jailed "(1..)", "(1..)"
165
+ end
166
+
167
+ def test_beginless_range
168
+ assert_jailed "(..5)", "(..5)"
169
+ end
170
+
68
171
  private
69
172
 
70
173
  def assert_jailed(expected, code)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: safemode
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sven Fuchs
@@ -10,112 +10,110 @@ authors:
10
10
  - Kingsley Hendrickse
11
11
  - Ohad Levy
12
12
  - Dmitri Dolguikh
13
- autorequire:
14
13
  bindir: bin
15
14
  cert_chain: []
16
- date: 2024-03-19 00:00:00.000000000 Z
15
+ date: 2026-05-21 00:00:00.000000000 Z
17
16
  dependencies:
18
17
  - !ruby/object:Gem::Dependency
19
18
  name: ruby2ruby
20
19
  requirement: !ruby/object:Gem::Requirement
21
20
  requirements:
22
- - - ">="
21
+ - - "~>"
23
22
  - !ruby/object:Gem::Version
24
- version: 2.4.0
23
+ version: '2.4'
25
24
  type: :runtime
26
25
  prerelease: false
27
26
  version_requirements: !ruby/object:Gem::Requirement
28
27
  requirements:
29
- - - ">="
28
+ - - "~>"
30
29
  - !ruby/object:Gem::Version
31
- version: 2.4.0
30
+ version: '2.4'
32
31
  - !ruby/object:Gem::Dependency
33
- name: ruby_parser
32
+ name: prism
34
33
  requirement: !ruby/object:Gem::Requirement
35
34
  requirements:
36
- - - ">="
35
+ - - "~>"
37
36
  - !ruby/object:Gem::Version
38
- version: 3.10.1
37
+ version: '1.0'
39
38
  type: :runtime
40
39
  prerelease: false
41
40
  version_requirements: !ruby/object:Gem::Requirement
42
41
  requirements:
43
- - - ">="
42
+ - - "~>"
44
43
  - !ruby/object:Gem::Version
45
- version: 3.10.1
44
+ version: '1.0'
46
45
  - !ruby/object:Gem::Dependency
47
46
  name: sexp_processor
48
47
  requirement: !ruby/object:Gem::Requirement
49
48
  requirements:
50
- - - ">="
49
+ - - "~>"
51
50
  - !ruby/object:Gem::Version
52
- version: 4.10.0
51
+ version: '4.10'
53
52
  type: :runtime
54
53
  prerelease: false
55
54
  version_requirements: !ruby/object:Gem::Requirement
56
55
  requirements:
57
- - - ">="
56
+ - - "~>"
58
57
  - !ruby/object:Gem::Version
59
- version: 4.10.0
58
+ version: '4.10'
60
59
  - !ruby/object:Gem::Dependency
61
60
  name: rake
62
61
  requirement: !ruby/object:Gem::Requirement
63
62
  requirements:
64
- - - ">="
63
+ - - "~>"
65
64
  - !ruby/object:Gem::Version
66
- version: '0'
65
+ version: '13.4'
67
66
  type: :development
68
67
  prerelease: false
69
68
  version_requirements: !ruby/object:Gem::Requirement
70
69
  requirements:
71
- - - ">="
70
+ - - "~>"
72
71
  - !ruby/object:Gem::Version
73
- version: '0'
72
+ version: '13.4'
74
73
  - !ruby/object:Gem::Dependency
75
74
  name: rdoc
76
75
  requirement: !ruby/object:Gem::Requirement
77
76
  requirements:
78
- - - ">="
77
+ - - "~>"
79
78
  - !ruby/object:Gem::Version
80
- version: '0'
79
+ version: '7.2'
81
80
  type: :development
82
81
  prerelease: false
83
82
  version_requirements: !ruby/object:Gem::Requirement
84
83
  requirements:
85
- - - ">="
84
+ - - "~>"
86
85
  - !ruby/object:Gem::Version
87
- version: '0'
86
+ version: '7.2'
88
87
  - !ruby/object:Gem::Dependency
89
88
  name: simplecov
90
89
  requirement: !ruby/object:Gem::Requirement
91
90
  requirements:
92
- - - ">="
91
+ - - "~>"
93
92
  - !ruby/object:Gem::Version
94
- version: '0'
93
+ version: '0.22'
95
94
  type: :development
96
95
  prerelease: false
97
96
  version_requirements: !ruby/object:Gem::Requirement
98
97
  requirements:
99
- - - ">="
98
+ - - "~>"
100
99
  - !ruby/object:Gem::Version
101
- version: '0'
100
+ version: '0.22'
102
101
  - !ruby/object:Gem::Dependency
103
102
  name: test-unit
104
103
  requirement: !ruby/object:Gem::Requirement
105
104
  requirements:
106
- - - ">="
105
+ - - "~>"
107
106
  - !ruby/object:Gem::Version
108
- version: '0'
107
+ version: '3.7'
109
108
  type: :development
110
109
  prerelease: false
111
110
  version_requirements: !ruby/object:Gem::Requirement
112
111
  requirements:
113
- - - ">="
112
+ - - "~>"
114
113
  - !ruby/object:Gem::Version
115
- version: '0'
114
+ version: '3.7'
116
115
  description: A library for safe evaluation of Ruby code based on RubyParser and Ruby2Ruby.
117
116
  Provides Rails ActionView template handlers for ERB and Haml.
118
- email:
119
117
  executables: []
120
118
  extensions: []
121
119
  extra_rdoc_files:
@@ -150,7 +148,6 @@ homepage: https://github.com/svenfuchs/safemode
150
148
  licenses:
151
149
  - MIT
152
150
  metadata: {}
153
- post_install_message:
154
151
  rdoc_options: []
155
152
  require_paths:
156
153
  - lib
@@ -158,19 +155,17 @@ required_ruby_version: !ruby/object:Gem::Requirement
158
155
  requirements:
159
156
  - - ">="
160
157
  - !ruby/object:Gem::Version
161
- version: '2.7'
158
+ version: '3.0'
162
159
  - - "<"
163
160
  - !ruby/object:Gem::Version
164
- version: '3.2'
161
+ version: '3.4'
165
162
  required_rubygems_version: !ruby/object:Gem::Requirement
166
163
  requirements:
167
164
  - - ">="
168
165
  - !ruby/object:Gem::Version
169
166
  version: '0'
170
167
  requirements: []
171
- rubygems_version: 3.4.10
172
- signing_key:
168
+ rubygems_version: 4.0.10
173
169
  specification_version: 4
174
- summary: A library for safe evaluation of Ruby code based on ParseTree/RubyParser
175
- and Ruby2Ruby
170
+ summary: A library for safe evaluation of Ruby code based on Prism and Ruby2Ruby
176
171
  test_files: []