rubyless 0.1.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.
- data/History.txt +4 -0
- data/README.rdoc +92 -0
- data/Rakefile +124 -0
- data/lib/RubyLess.rb +304 -0
- data/lib/SafeClass.rb +98 -0
- data/rubyless.gemspec +30 -0
- data/test/RubyLess/basic.yml +81 -0
- data/test/RubyLess/errors.yml +16 -0
- data/test/RubyLess_test.rb +65 -0
- data/test/mock/dummy_class.rb +35 -0
- data/test/test_helper.rb +6 -0
- metadata +66 -0
data/History.txt
ADDED
data/README.rdoc
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
= RubyLess
|
2
|
+
|
3
|
+
* http://zenadmin.org/546
|
4
|
+
|
5
|
+
== DESCRIPTION:
|
6
|
+
|
7
|
+
RubyLess is an interpreter for "safe ruby". The idea is to transform some "unsafe" ruby code into safe, type checked
|
8
|
+
ruby, eventually rewriting some variables or methods. The goals:
|
9
|
+
|
10
|
+
1. give ruby scripting access to users without any security risk
|
11
|
+
2. rewrite variable names depending on compilation context
|
12
|
+
3. never raise runtime errors through compile time type checking and powerful nil handling
|
13
|
+
|
14
|
+
This library is based on Ruby2Ruby by Ryan Davis, thanks to him for sharing his work.
|
15
|
+
|
16
|
+
== SYNOPSIS:
|
17
|
+
|
18
|
+
For every class that will be involved in your RubyLess scripts, you need to declare safe methods with the 'safe_method' macro if
|
19
|
+
you want to enable methods from this class. You have to specify the return type of the method. If you have some methods that
|
20
|
+
return 'nil' instead of the declared output, you need to wrap your final ruby 'eval' with a rescue clause.
|
21
|
+
|
22
|
+
# signature is made of [method, arg_class, arg_class, ...]
|
23
|
+
class Node
|
24
|
+
include RubyLess::SafeClass
|
25
|
+
safe_method [:ancestor?, Node] => RubyLess::Boolean
|
26
|
+
end
|
27
|
+
|
28
|
+
# methods defined in helper
|
29
|
+
|
30
|
+
# global methods
|
31
|
+
include RubyLess::SafeClass
|
32
|
+
safe_method :prev => {:class => Dummy, :method => 'previous', :nil => true}
|
33
|
+
safe_method :node => lambda {|h| {:class => h.context[:node_class], :method => h.context[:node]}}
|
34
|
+
safe_method [:strftime, Time, String] => String
|
35
|
+
safe_method_for String, [:==, String] => RubyLess::Boolean
|
36
|
+
safe_method_for String, [:to_s] => String
|
37
|
+
|
38
|
+
You can also redefine 'safe_method?' for any class or for the main helper in order to do some more complicated renaming. Note
|
39
|
+
also that you should add ':nil => true' declaration to any method that could return a nil value so that RubyLess can render
|
40
|
+
code that will not break during runtime (adding nil checking in the form of "foo ? foo.name : nil").
|
41
|
+
|
42
|
+
You can now parse some ruby code:
|
43
|
+
|
44
|
+
RubyLess.translate("!prev.ancestor?(main) && !node.ancestor?(main)", self)
|
45
|
+
=> "(not previous.ancestor?(@node) and not var1.ancestor?(@node))"
|
46
|
+
|
47
|
+
RubyLess.translate("id > 45 and (3 > -id or 3+3)", self)
|
48
|
+
=> "(var1.zip>45 and ((3>-var1.zip) or (3+3)))"
|
49
|
+
|
50
|
+
RubyLess.translate("strftime(now, '%Y')", self)
|
51
|
+
=> "strftime(Time.now, \"%Y\")"
|
52
|
+
|
53
|
+
RubyLess.translate("log_info(spouse, spouse.name)", self)
|
54
|
+
=> "(var1.spouse ? log_info(var1.spouse, var1.spouse.name) : nil)"
|
55
|
+
|
56
|
+
Since most of the code in SafeClass is string evaluated (to scope class variables), there is not much to parse for rdoc. You
|
57
|
+
can look at the tests for an idea of how to declare things. If you have more questions, ask on zena's mailing list:
|
58
|
+
|
59
|
+
http://zenadmin.org/community
|
60
|
+
|
61
|
+
== REQUIREMENTS:
|
62
|
+
|
63
|
+
* parse_tree
|
64
|
+
|
65
|
+
== INSTALL:
|
66
|
+
|
67
|
+
sudo gem install rubyless
|
68
|
+
|
69
|
+
== LICENSE:
|
70
|
+
|
71
|
+
(The MIT License)
|
72
|
+
|
73
|
+
Copyright (c) 2009 Gaspard Bucher
|
74
|
+
|
75
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
76
|
+
a copy of this software and associated documentation files (the
|
77
|
+
'Software'), to deal in the Software without restriction, including
|
78
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
79
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
80
|
+
permit persons to whom the Software is furnished to do so, subject to
|
81
|
+
the following conditions:
|
82
|
+
|
83
|
+
The above copyright notice and this permission notice shall be
|
84
|
+
included in all copies or substantial portions of the Software.
|
85
|
+
|
86
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
87
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
88
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
89
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
90
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
91
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
92
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,124 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
require "rake/gempackagetask"
|
3
|
+
require "rake/rdoctask"
|
4
|
+
require "lib/RubyLess"
|
5
|
+
|
6
|
+
task :default => :test
|
7
|
+
|
8
|
+
require "rake/testtask"
|
9
|
+
Rake::TestTask.new do |t|
|
10
|
+
t.libs << "test"
|
11
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
12
|
+
t.verbose = true
|
13
|
+
end
|
14
|
+
|
15
|
+
# This builds the actual gem. For details of what all these options
|
16
|
+
# mean, and other ones you can add, check the documentation here:
|
17
|
+
#
|
18
|
+
# http://rubygems.org/read/chapter/20
|
19
|
+
#
|
20
|
+
spec = Gem::Specification.new do |s|
|
21
|
+
|
22
|
+
# Change these as appropriate
|
23
|
+
s.name = "rubyless"
|
24
|
+
s.version = RubyLess::VERSION
|
25
|
+
s.summary = %q{RubyLess is an interpreter for "safe ruby". The idea is to transform some "unsafe" ruby code into safe, type checked
|
26
|
+
ruby, eventually rewriting some variables or methods}
|
27
|
+
s.author = "Gaspard Bucher"
|
28
|
+
s.email = "gaspard@teti.ch"
|
29
|
+
s.homepage = "http://zenadmin.org/546"
|
30
|
+
|
31
|
+
s.has_rdoc = true
|
32
|
+
s.extra_rdoc_files = %w(README.rdoc)
|
33
|
+
s.rdoc_options = %w(--main README.rdoc)
|
34
|
+
|
35
|
+
# Add any extra files to include in the gem
|
36
|
+
s.files = %w(History.txt Rakefile README.rdoc rubyless.gemspec) + Dir.glob("{test,lib}/**/*")
|
37
|
+
|
38
|
+
s.require_paths = ["lib"]
|
39
|
+
|
40
|
+
# If you want to depend on other gems, add them here, along with any
|
41
|
+
# relevant versions
|
42
|
+
# s.add_dependency("some_other_gem", "~> 0.1.0")
|
43
|
+
|
44
|
+
# If your tests use any gems, include them here
|
45
|
+
# s.add_development_dependency("mocha")
|
46
|
+
|
47
|
+
# If you want to publish automatically to rubyforge, you'll may need
|
48
|
+
# to tweak this, and the publishing task below too.
|
49
|
+
s.rubyforge_project = "rubyless"
|
50
|
+
end
|
51
|
+
|
52
|
+
# This task actually builds the gem. We also regenerate a static
|
53
|
+
# .gemspec file, which is useful if something (i.e. GitHub) will
|
54
|
+
# be automatically building a gem for this project. If you're not
|
55
|
+
# using GitHub, edit as appropriate.
|
56
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
57
|
+
pkg.gem_spec = spec
|
58
|
+
|
59
|
+
# Generate the gemspec file for github.
|
60
|
+
file = File.dirname(__FILE__) + "/#{spec.name}.gemspec"
|
61
|
+
File.open(file, "w") {|f| f << spec.to_ruby }
|
62
|
+
end
|
63
|
+
|
64
|
+
# Generate documentation
|
65
|
+
Rake::RDocTask.new do |rd|
|
66
|
+
rd.main = "README.rdoc"
|
67
|
+
rd.rdoc_files.include("README.rdoc", "lib/**/*.rb")
|
68
|
+
rd.rdoc_dir = "rdoc"
|
69
|
+
end
|
70
|
+
|
71
|
+
desc 'Clear out RDoc and generated packages'
|
72
|
+
task :clean => [:clobber_rdoc, :clobber_package] do
|
73
|
+
rm "#{spec.name}.gemspec"
|
74
|
+
end
|
75
|
+
|
76
|
+
# If you want to publish to RubyForge automatically, here's a simple
|
77
|
+
# task to help do that. If you don't, just get rid of this.
|
78
|
+
# Be sure to set up your Rubyforge account details with the Rubyforge
|
79
|
+
# gem; you'll need to run `rubyforge setup` and `rubyforge config` at
|
80
|
+
# the very least.
|
81
|
+
begin
|
82
|
+
require "rake/contrib/sshpublisher"
|
83
|
+
namespace :rubyforge do
|
84
|
+
|
85
|
+
desc "Release gem and RDoc documentation to RubyForge"
|
86
|
+
task :release => ["rubyforge:release:gem", "rubyforge:release:docs"]
|
87
|
+
|
88
|
+
namespace :release do
|
89
|
+
desc "Release a new version of this gem"
|
90
|
+
task :gem => [:package] do
|
91
|
+
require 'rubyforge'
|
92
|
+
rubyforge = RubyForge.new
|
93
|
+
rubyforge.configure
|
94
|
+
rubyforge.login
|
95
|
+
rubyforge.userconfig['release_notes'] = spec.summary
|
96
|
+
path_to_gem = File.join(File.dirname(__FILE__), "pkg", "#{spec.name}-#{spec.version}.gem")
|
97
|
+
puts "Publishing #{spec.name}-#{spec.version.to_s} to Rubyforge..."
|
98
|
+
rubyforge.add_release(spec.rubyforge_project, spec.name, spec.version.to_s, path_to_gem)
|
99
|
+
end
|
100
|
+
|
101
|
+
desc "Publish RDoc to RubyForge."
|
102
|
+
task :docs => [:rdoc] do
|
103
|
+
config = YAML.load(
|
104
|
+
File.read(File.expand_path('~/.rubyforge/user-config.yml'))
|
105
|
+
)
|
106
|
+
|
107
|
+
host = "#{config['username']}@rubyforge.org"
|
108
|
+
remote_dir = "/var/www/gforge-projects/rubyless/" # Should be the same as the rubyforge project name
|
109
|
+
local_dir = 'rdoc'
|
110
|
+
|
111
|
+
Rake::SshDirPublisher.new(host, remote_dir, local_dir).upload
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
rescue LoadError
|
116
|
+
puts "Rake SshDirPublisher is unavailable or your rubyforge environment is not configured."
|
117
|
+
end
|
118
|
+
|
119
|
+
desc 'Generate the gemspec to serve this Gem from Github'
|
120
|
+
task :github do
|
121
|
+
file = File.dirname(__FILE__) + "/#{spec.name}.gemspec"
|
122
|
+
File.open(file, 'w') {|f| f << spec.to_ruby }
|
123
|
+
puts "Created gemspec: #{file}"
|
124
|
+
end
|
data/lib/RubyLess.rb
ADDED
@@ -0,0 +1,304 @@
|
|
1
|
+
$:.unshift(File.dirname(__FILE__)) unless
|
2
|
+
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
|
3
|
+
|
4
|
+
require 'rubygems'
|
5
|
+
require 'parse_tree'
|
6
|
+
require 'SafeClass'
|
7
|
+
=begin rdoc
|
8
|
+
=end
|
9
|
+
module RubyLess
|
10
|
+
VERSION = '0.1.0'
|
11
|
+
|
12
|
+
def self.translate(string, helper)
|
13
|
+
RubyLessProcessor.translate(string, helper)
|
14
|
+
end
|
15
|
+
|
16
|
+
class Boolean
|
17
|
+
end
|
18
|
+
|
19
|
+
class Number
|
20
|
+
include SafeClass
|
21
|
+
safe_method( [:==, Number] => Boolean, [:< , Number] => Boolean, [:> , Number] => Boolean, [:<=, Number] => Boolean, [:>=, Number] => Boolean,
|
22
|
+
[:- , Number] => Number, [:+ , Number] => Number, [:* , Number] => Number, [:/ , Number] => Number,
|
23
|
+
[:% , Number] => Number, [:"-@"] => Number )
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
class Missing
|
28
|
+
[:==, :< , :> , :<=, :>=, :"?"].each do |sym|
|
29
|
+
define_method(sym) do |arg|
|
30
|
+
false
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_s
|
35
|
+
''
|
36
|
+
end
|
37
|
+
|
38
|
+
def nil?
|
39
|
+
true
|
40
|
+
end
|
41
|
+
|
42
|
+
def method_missing(*meth)
|
43
|
+
self
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
Nil = Missing.new
|
48
|
+
|
49
|
+
class TypedString < String
|
50
|
+
attr_reader :klass, :opts
|
51
|
+
|
52
|
+
def initialize(content = "", opts = nil)
|
53
|
+
opts ||= {:class => String}
|
54
|
+
replace(content)
|
55
|
+
@opts = opts.dup
|
56
|
+
if could_be_nil? && !@opts[:cond]
|
57
|
+
@opts[:cond] = [self.to_s]
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def klass
|
62
|
+
@opts[:class]
|
63
|
+
end
|
64
|
+
|
65
|
+
def could_be_nil?
|
66
|
+
@opts[:nil]
|
67
|
+
end
|
68
|
+
|
69
|
+
# condition when 'could_be_nil' comes from a different method then the last one:
|
70
|
+
# var1.spouse.name == ''
|
71
|
+
# "var1.spouse" would be the condition that inserted 'could_be_nil?'.
|
72
|
+
def cond
|
73
|
+
@opts[:cond]
|
74
|
+
end
|
75
|
+
|
76
|
+
# raw result without nil checking:
|
77
|
+
# "var1.spouse.name" instead of "(var1.spouse ? var1.spouse.name : nil)"
|
78
|
+
def raw
|
79
|
+
@opts[:raw] || self.to_s
|
80
|
+
end
|
81
|
+
|
82
|
+
def <<(typed_string)
|
83
|
+
append_opts(typed_string)
|
84
|
+
if self.empty?
|
85
|
+
replace(typed_string.raw)
|
86
|
+
else
|
87
|
+
replace("#{self.raw}, #{typed_string.raw}")
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def append_opts(typed_string)
|
92
|
+
if self.empty?
|
93
|
+
@opts = typed_string.opts.dup
|
94
|
+
else
|
95
|
+
if klass.kind_of?(Array)
|
96
|
+
klass << typed_string.klass
|
97
|
+
else
|
98
|
+
@opts[:class] = [klass, typed_string.klass]
|
99
|
+
end
|
100
|
+
append_cond(typed_string.cond) if typed_string.could_be_nil?
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def append_cond(condition)
|
105
|
+
@opts[:cond] ||= []
|
106
|
+
@opts[:cond] += [condition].flatten
|
107
|
+
@opts[:cond].uniq!
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
class RubyLessProcessor < SexpProcessor
|
112
|
+
attr_reader :ruby
|
113
|
+
|
114
|
+
INFIX_OPERATOR = [:"<=>", :==, :<, :>, :<=, :>=, :-, :+, :*, :/, :%]
|
115
|
+
PREFIX_OPERATOR = [:"-@"]
|
116
|
+
|
117
|
+
def self.translate(string, helper)
|
118
|
+
sexp = ParseTree.translate(string)
|
119
|
+
self.new(helper).process(sexp)
|
120
|
+
end
|
121
|
+
|
122
|
+
def initialize(helper)
|
123
|
+
super()
|
124
|
+
@helper = helper
|
125
|
+
@indent = " "
|
126
|
+
self.auto_shift_type = true
|
127
|
+
self.strict = true
|
128
|
+
self.expected = TypedString
|
129
|
+
end
|
130
|
+
|
131
|
+
def process_and(exp)
|
132
|
+
t "(#{process(exp.shift)} and #{process(exp.shift)})", Boolean
|
133
|
+
end
|
134
|
+
|
135
|
+
def process_or(exp)
|
136
|
+
t "(#{process(exp.shift)} or #{process(exp.shift)})", Boolean
|
137
|
+
end
|
138
|
+
|
139
|
+
def process_not(exp)
|
140
|
+
t "not #{process(exp.shift)}", Boolean
|
141
|
+
end
|
142
|
+
|
143
|
+
def process_if(exp)
|
144
|
+
cond = process(exp.shift)
|
145
|
+
true_res = process(exp.shift)
|
146
|
+
false_res = process(exp.shift)
|
147
|
+
|
148
|
+
if true_res && false_res && true_res.klass != false_res.klass
|
149
|
+
raise "Error in conditional expression: '#{true_res}' and '#{false_res}' do not return results of same type (#{true_res.klass} != #{false_res.klass})."
|
150
|
+
end
|
151
|
+
raise "Error in conditional expression." unless true_res || false_res
|
152
|
+
opts = {}
|
153
|
+
opts[:nil] = true_res.nil? || true_res.could_be_nil? || false_res.nil? || false_res.could_be_nil?
|
154
|
+
opts[:class] = true_res ? true_res.klass : false_res.klass
|
155
|
+
t "#{cond} ? #{true_res || 'nil'} : #{false_res || 'nil'}", opts
|
156
|
+
end
|
157
|
+
|
158
|
+
def process_call(exp)
|
159
|
+
receiver_node_type = exp.first.nil? ? nil : exp.first.first
|
160
|
+
receiver = process exp.shift
|
161
|
+
|
162
|
+
# receiver = t("(#{receiver})", receiver.klass) if
|
163
|
+
# Ruby2Ruby::ASSIGN_NODES.include? receiver_node_type
|
164
|
+
|
165
|
+
method_call(receiver, exp)
|
166
|
+
end
|
167
|
+
|
168
|
+
def process_fcall(exp)
|
169
|
+
method_call(nil, exp)
|
170
|
+
end
|
171
|
+
|
172
|
+
def process_arglist(exp)
|
173
|
+
code = t("")
|
174
|
+
until exp.empty? do
|
175
|
+
code << process(exp.shift)
|
176
|
+
end
|
177
|
+
code
|
178
|
+
end
|
179
|
+
|
180
|
+
def process_array(exp)
|
181
|
+
res = process_arglist(exp)
|
182
|
+
exp.size > 1 ? t("[#{res}]", res.opts) : res
|
183
|
+
end
|
184
|
+
|
185
|
+
def process_vcall(exp)
|
186
|
+
var_name = exp.shift
|
187
|
+
unless opts = get_method([var_name], @helper, false)
|
188
|
+
raise "Unknown variable or method '#{var_name}'."
|
189
|
+
end
|
190
|
+
method = opts[:method] || var_name.to_s
|
191
|
+
t method, opts
|
192
|
+
end
|
193
|
+
|
194
|
+
def process_lit(exp)
|
195
|
+
t exp.shift.to_s, Number
|
196
|
+
end
|
197
|
+
|
198
|
+
def process_str(exp)
|
199
|
+
t exp.shift.inspect, String
|
200
|
+
end
|
201
|
+
|
202
|
+
def process_dstr(exp)
|
203
|
+
t "\"#{parse_dstr(exp)}\"", String
|
204
|
+
end
|
205
|
+
|
206
|
+
def process_evstr(exp)
|
207
|
+
exp.empty? ? t('', String) : process(exp.shift)
|
208
|
+
end
|
209
|
+
|
210
|
+
private
|
211
|
+
def t(content, opts = nil)
|
212
|
+
if opts.nil?
|
213
|
+
opts = {:class => String}
|
214
|
+
elsif !opts.kind_of?(Hash)
|
215
|
+
opts = {:class => opts}
|
216
|
+
end
|
217
|
+
TypedString.new(content, opts)
|
218
|
+
end
|
219
|
+
|
220
|
+
def t_if(cond, true_res, opts)
|
221
|
+
if cond != []
|
222
|
+
if cond.size > 1
|
223
|
+
condition = "(#{cond.join(' && ')})"
|
224
|
+
else
|
225
|
+
condition = cond.join('')
|
226
|
+
end
|
227
|
+
|
228
|
+
# we can append to 'raw'
|
229
|
+
if opts[:nil]
|
230
|
+
# applied method could produce a nil value (so we cannot concat method on top of 'raw' and only check previous condition)
|
231
|
+
t "(#{condition} ? #{true_res} : nil)", opts
|
232
|
+
else
|
233
|
+
# we can keep on checking only 'condition' and appending methods to 'raw'
|
234
|
+
t "(#{condition} ? #{true_res} : nil)", opts.merge(:nil => true, :cond => cond, :raw => true_res)
|
235
|
+
end
|
236
|
+
else
|
237
|
+
t true_res, opts
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
def method_call(receiver, exp)
|
242
|
+
method = exp.shift
|
243
|
+
if args = exp.shift rescue nil
|
244
|
+
args = process args || []
|
245
|
+
signature = [method] + [args.klass].flatten ## FIXME: error prone !
|
246
|
+
# execution conditional
|
247
|
+
cond = args.cond || []
|
248
|
+
else
|
249
|
+
args = []
|
250
|
+
signature = [method]
|
251
|
+
cond = []
|
252
|
+
end
|
253
|
+
|
254
|
+
if receiver
|
255
|
+
if receiver.could_be_nil?
|
256
|
+
cond += receiver.cond
|
257
|
+
end
|
258
|
+
raise "'#{receiver}' does not respond to '#{method}(#{args.raw})'." unless opts = get_method(signature, receiver.klass)
|
259
|
+
method = opts[:method] if opts[:method]
|
260
|
+
if method == :/
|
261
|
+
t_if cond, "(#{receiver.raw}#{method}#{args.raw} rescue nil)", opts.merge(:nil => true)
|
262
|
+
elsif INFIX_OPERATOR.include?(method)
|
263
|
+
t_if cond, "(#{receiver.raw}#{method}#{args.raw})", opts
|
264
|
+
elsif PREFIX_OPERATOR.include?(method)
|
265
|
+
t_if cond, "#{method.to_s[0..0]}#{receiver.raw}", opts
|
266
|
+
else
|
267
|
+
args = "(#{args.raw})" if args != []
|
268
|
+
t_if cond, "#{receiver.raw}.#{method}#{args}", opts
|
269
|
+
end
|
270
|
+
else
|
271
|
+
raise "Unknown method '#{method}(#{args.raw})'." unless opts = get_method(signature, @helper, false)
|
272
|
+
method = opts[:method] if opts[:method]
|
273
|
+
args = "(#{args.raw})" if args != []
|
274
|
+
t_if cond, "#{method}#{args}", opts
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
def parse_dstr(exp, in_regex = false)
|
279
|
+
res = escape_str(exp.shift, in_regex)
|
280
|
+
|
281
|
+
while part = exp.shift
|
282
|
+
case part.first
|
283
|
+
when :str then
|
284
|
+
res << escape_str(part.last, in_regex)
|
285
|
+
else
|
286
|
+
res << '#{' << process(part) << '}'
|
287
|
+
end
|
288
|
+
end
|
289
|
+
res
|
290
|
+
end
|
291
|
+
|
292
|
+
def escape_str(str, in_regex = false)
|
293
|
+
res = str.gsub(/"/, '\"').gsub(/\n/, '\n')
|
294
|
+
res.gsub!(/\//, '\/') if in_regex
|
295
|
+
res
|
296
|
+
end
|
297
|
+
|
298
|
+
def get_method(signature, receiver, is_method = true)
|
299
|
+
res = receiver.respond_to?(:safe_method?) ? receiver.safe_method?(signature) : @helper.class.safe_method_for?(receiver, signature)
|
300
|
+
res = res.call(@helper) if res.kind_of?(Proc)
|
301
|
+
res
|
302
|
+
end
|
303
|
+
end
|
304
|
+
end
|
data/lib/SafeClass.rb
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
module RubyLess
|
2
|
+
module SafeClass
|
3
|
+
def self.included(base)
|
4
|
+
# add all methods from the module "AddActsAsMethod" to the 'base' module
|
5
|
+
base.class_eval <<-END
|
6
|
+
@@_safe_methods ||= {} # defined for each class
|
7
|
+
@@_safe_methods_all ||= {} # full list with inherited attributes
|
8
|
+
|
9
|
+
def self.safe_method(hash)
|
10
|
+
list = (@@_safe_methods[self] ||= {})
|
11
|
+
hash.each do |k,v|
|
12
|
+
k = [k] unless k.kind_of?(Array)
|
13
|
+
v = {:class => v} unless v.kind_of?(Hash) || v.kind_of?(Proc)
|
14
|
+
list[k] = v
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.safe_methods
|
19
|
+
safe_methods_for(self)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.safe_methods_for(klass)
|
23
|
+
@@_safe_methods_all[klass] ||= build_safe_methods_list(klass)
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.build_safe_methods_list(klass)
|
27
|
+
list = klass.superclass.respond_to?(:safe_methods) ? klass.superclass.safe_methods : {}
|
28
|
+
(@@_safe_methods[klass] || {}).map do |signature, return_value|
|
29
|
+
if return_value.kind_of?(Hash)
|
30
|
+
return_value[:class] = parse_class(return_value[:class])
|
31
|
+
elsif !return_value.kind_of?(Proc)
|
32
|
+
return_value = {:class => return_value}
|
33
|
+
end
|
34
|
+
signature.map! {|e| parse_class(e)}
|
35
|
+
list[signature] = return_value
|
36
|
+
end
|
37
|
+
list
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.safe_method?(signature)
|
41
|
+
if res = safe_methods[signature]
|
42
|
+
res.dup
|
43
|
+
else
|
44
|
+
nil
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.safe_method_for?(klass, signature)
|
49
|
+
if res = safe_methods_for(klass)[signature]
|
50
|
+
res.dup
|
51
|
+
else
|
52
|
+
nil
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def safe_method?(signature)
|
57
|
+
self.class.safe_methods[signature]
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.safe_method_for(klass, hash)
|
61
|
+
list = (@@_safe_methods[klass] ||= {})
|
62
|
+
hash.each do |k,v|
|
63
|
+
k = [k] unless k.kind_of?(Array)
|
64
|
+
v = {:class => v} unless v.kind_of?(Hash) || v.kind_of?(Proc)
|
65
|
+
list[k] = v
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.parse_class(klass)
|
70
|
+
if klass.kind_of?(Array)
|
71
|
+
if klass[0].kind_of?(String)
|
72
|
+
[Module::const_get(klass[0])]
|
73
|
+
else
|
74
|
+
klass
|
75
|
+
end
|
76
|
+
else
|
77
|
+
if klass.kind_of?(String)
|
78
|
+
Module::const_get(klass)
|
79
|
+
else
|
80
|
+
klass
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.safe_attribute?(sym)
|
86
|
+
column_names.include?(sym) || zafu_readable?(sym) || safe_attribute_list.include?(sym.to_s)
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.zafu_readable?(sym)
|
90
|
+
if sym.to_s =~ /(.*)_zips?$/
|
91
|
+
return true if self.ancestors.include?(Node) && RelationProxy.find_by_role($1.singularize)
|
92
|
+
end
|
93
|
+
self.zafu_readable_attributes.include?(sym.to_s)
|
94
|
+
end
|
95
|
+
END
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
data/rubyless.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = %q{rubyless}
|
5
|
+
s.version = "0.1.0"
|
6
|
+
|
7
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
8
|
+
s.authors = ["Gaspard Bucher"]
|
9
|
+
s.date = %q{2009-06-02}
|
10
|
+
s.email = %q{gaspard@teti.ch}
|
11
|
+
s.extra_rdoc_files = ["README.rdoc"]
|
12
|
+
s.files = ["History.txt", "Rakefile", "README.rdoc", "rubyless.gemspec", "test/mock", "test/mock/dummy_class.rb", "test/RubyLess", "test/RubyLess/basic.yml", "test/RubyLess/errors.yml", "test/RubyLess_test.rb", "test/test_helper.rb", "lib/RubyLess.rb", "lib/SafeClass.rb"]
|
13
|
+
s.has_rdoc = true
|
14
|
+
s.homepage = %q{http://zenadmin.org/546}
|
15
|
+
s.rdoc_options = ["--main", "README.rdoc"]
|
16
|
+
s.require_paths = ["lib"]
|
17
|
+
s.rubyforge_project = %q{rubyless}
|
18
|
+
s.rubygems_version = %q{1.3.1}
|
19
|
+
s.summary = %q{RubyLess is an interpreter for "safe ruby". The idea is to transform some "unsafe" ruby code into safe, type checked ruby, eventually rewriting some variables or methods}
|
20
|
+
|
21
|
+
if s.respond_to? :specification_version then
|
22
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
23
|
+
s.specification_version = 2
|
24
|
+
|
25
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
26
|
+
else
|
27
|
+
end
|
28
|
+
else
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
empty:
|
2
|
+
src: ""
|
3
|
+
tem: null
|
4
|
+
|
5
|
+
numbers:
|
6
|
+
src: "id > 45 and (3 > -id or 3+3)"
|
7
|
+
tem: "((var1.zip>45) and ((3>-var1.zip) or (3+3)))"
|
8
|
+
|
9
|
+
global_method:
|
10
|
+
src: "strftime(now, '%Y')"
|
11
|
+
tem: "strftime(Time.now, \"%Y\")"
|
12
|
+
|
13
|
+
dynamic_string:
|
14
|
+
src: "strftime(now, \"#{name}\")"
|
15
|
+
tem: "strftime(Time.now, \"#{var1.name}\")"
|
16
|
+
|
17
|
+
dynamic_string_again:
|
18
|
+
src: "strftime(now, \"#{name}\")"
|
19
|
+
tem: "strftime(Time.now, \"#{var1.name}\")"
|
20
|
+
|
21
|
+
rewrite_variables:
|
22
|
+
src: "!prev.ancestor?(main) && !node.ancestor?(main)"
|
23
|
+
tem: "(not previous.ancestor?(@node) and not var1.ancestor?(@node))"
|
24
|
+
|
25
|
+
method_can_return_nil:
|
26
|
+
src: "spouse.name"
|
27
|
+
tem: "(var1.spouse ? var1.spouse.name : nil)"
|
28
|
+
|
29
|
+
method_on_method_can_return_nil:
|
30
|
+
src: "spouse.name == 'yo'"
|
31
|
+
tem: "(var1.spouse ? (var1.spouse.name==\"yo\") : nil)"
|
32
|
+
res: ""
|
33
|
+
|
34
|
+
nil_greater_then:
|
35
|
+
src: "spouse.id > 1"
|
36
|
+
tem: "(var1.spouse ? (var1.spouse.zip>1) : nil)"
|
37
|
+
|
38
|
+
nil_ternary_op:
|
39
|
+
src: "spouse ? 'foo' : 'bar'"
|
40
|
+
tem: "var1.spouse ? \"foo\" : \"bar\""
|
41
|
+
res: 'bar'
|
42
|
+
|
43
|
+
nested_ternary_op:
|
44
|
+
src: "spouse.name == 'Adam' ? 'man' : 'not a man'"
|
45
|
+
tem: "(var1.spouse ? (var1.spouse.name==\"Adam\") : nil) ? \"man\" : \"not a man\""
|
46
|
+
res: "not a man"
|
47
|
+
|
48
|
+
method_on_method:
|
49
|
+
src: "project.name.to_s"
|
50
|
+
tem: "var1.project.name.to_s"
|
51
|
+
res: 'project'
|
52
|
+
|
53
|
+
comp_ternary_op:
|
54
|
+
src: "1 > 2 ? 'foo' : 'bar'"
|
55
|
+
tem: "(1>2) ? \"foo\" : \"bar\""
|
56
|
+
res: "bar"
|
57
|
+
|
58
|
+
method_ternary_op:
|
59
|
+
src: "id > 2 ? 'foo' : 'bar'"
|
60
|
+
tem: "(var1.zip>2) ? \"foo\" : \"bar\""
|
61
|
+
res: "foo"
|
62
|
+
|
63
|
+
method_argument_can_be_nil:
|
64
|
+
src: "vowel_count(spouse.name)"
|
65
|
+
tem: "(var1.spouse ? vowel_count(var1.spouse.name) : nil)"
|
66
|
+
|
67
|
+
multi_arg_method_argument_can_be_nil:
|
68
|
+
src: "log_info(spouse, 'foobar')"
|
69
|
+
tem: "(var1.spouse ? log_info(var1.spouse, \"foobar\") : nil)"
|
70
|
+
|
71
|
+
multi_arg_method_arguments_can_be_nil:
|
72
|
+
src: "log_info(husband, spouse.name)"
|
73
|
+
tem: "((var1.husband && var1.spouse) ? log_info(var1.husband, var1.spouse.name) : nil)"
|
74
|
+
|
75
|
+
multi_arg_method_arguments_can_be_nil_same_condition:
|
76
|
+
src: "log_info(spouse, spouse.name)"
|
77
|
+
tem: "(var1.spouse ? log_info(var1.spouse, var1.spouse.name) : nil)"
|
78
|
+
|
79
|
+
literal_argument_for_method:
|
80
|
+
src: "vowel_count('ruby')"
|
81
|
+
res: "2"
|
@@ -0,0 +1,16 @@
|
|
1
|
+
unknown_global_method:
|
2
|
+
src: "system('echo date')"
|
3
|
+
res: "Unknown method 'system(\"echo date\")'."
|
4
|
+
|
5
|
+
bad_argument_types:
|
6
|
+
src: "strftime(34,'ffoo')"
|
7
|
+
res: "Unknown method 'strftime(34, \"ffoo\")'."
|
8
|
+
|
9
|
+
zero_div:
|
10
|
+
src: "1/(id-10)"
|
11
|
+
tem: "(1/(var1.zip-10) rescue nil)"
|
12
|
+
res: ""
|
13
|
+
|
14
|
+
no_looping:
|
15
|
+
src: "while(true) do puts 'flood' end"
|
16
|
+
res: 'Bug! Unknown node-type :while to RubyLess::RubyLessProcessor'
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper.rb'
|
2
|
+
|
3
|
+
class SimpleHelper < Test::Unit::TestCase
|
4
|
+
attr_reader :context
|
5
|
+
yamltest :src_from_title => false
|
6
|
+
include RubyLess::SafeClass
|
7
|
+
safe_method :prev => {:class => Dummy, :method => 'previous'}
|
8
|
+
safe_method :main => {:class => Dummy, :method => '@node'}
|
9
|
+
safe_method :node => lambda {|h| {:class => h.context[:node_class], :method => h.context[:node]}}
|
10
|
+
safe_method :now => {:class => Time, :method => 'Time.now'}
|
11
|
+
safe_method [:strftime, Time, String] => String
|
12
|
+
safe_method [:vowel_count, String] => RubyLess::Number
|
13
|
+
safe_method [:log_info, Dummy, String] => String
|
14
|
+
safe_method_for String, [:==, String] => RubyLess::Boolean
|
15
|
+
safe_method_for String, [:to_s] => String
|
16
|
+
|
17
|
+
def safe_method?(signature)
|
18
|
+
unless res = self.class.safe_method?(signature)
|
19
|
+
# try to execute method in the current var "var.method"
|
20
|
+
if res = context[:node_class].safe_method?(signature)
|
21
|
+
res = res.call(self) if res.kind_of?(Proc)
|
22
|
+
res[:method] = "#{context[:node]}.#{res[:method] || signature[0]}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
res
|
26
|
+
end
|
27
|
+
|
28
|
+
def var1
|
29
|
+
Dummy.new
|
30
|
+
end
|
31
|
+
|
32
|
+
def vowel_count(str)
|
33
|
+
str.tr('^aeiouy', '').size
|
34
|
+
end
|
35
|
+
|
36
|
+
def log_info(obj, msg)
|
37
|
+
"[#{obj.name}] #{msg}"
|
38
|
+
end
|
39
|
+
|
40
|
+
def yt_do_test(file, test, context = yt_get('context',file,test))
|
41
|
+
@@test_strings[file][test].keys.each do |key|
|
42
|
+
next if ['src', 'context'].include?(key)
|
43
|
+
yt_assert yt_get(key,file,test), parse(key, file, test, context)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def parse(key, file, test, opts)
|
48
|
+
@context = {:node => 'var1', :node_class => Dummy}
|
49
|
+
source = yt_get('src', file, test)
|
50
|
+
case key
|
51
|
+
when 'tem'
|
52
|
+
source ? RubyLess.translate(source, self) : yt_get('tem', file, test)
|
53
|
+
when 'res'
|
54
|
+
eval(source ? RubyLess.translate(source, self) : yt_get('tem', file, test)).to_s
|
55
|
+
else
|
56
|
+
"Unknown key '#{key}'. Should be 'tem' or 'res'."
|
57
|
+
end
|
58
|
+
rescue => err
|
59
|
+
# puts "\n\n#{err.message}"
|
60
|
+
# puts err.backtrace
|
61
|
+
err.message
|
62
|
+
end
|
63
|
+
|
64
|
+
yt_make
|
65
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
class Dummy
|
2
|
+
attr_reader :name
|
3
|
+
include RubyLess::SafeClass
|
4
|
+
|
5
|
+
safe_method [:ancestor?, Dummy] => RubyLess::Boolean
|
6
|
+
safe_method :parent => {:class => 'Dummy', :special_option => 'foobar'}
|
7
|
+
safe_method :children => ['Dummy']
|
8
|
+
safe_method :project => 'Dummy'
|
9
|
+
safe_method :spouse => {:class => 'Dummy', :nil => true}
|
10
|
+
safe_method :husband => {:class => 'Dummy', :nil => true}
|
11
|
+
safe_method :id => {:class => RubyLess::Number, :method => :zip}
|
12
|
+
safe_method :name => String
|
13
|
+
|
14
|
+
def initialize(name = 'dummy')
|
15
|
+
@name = name
|
16
|
+
end
|
17
|
+
|
18
|
+
# This method returns pseudo-nil and does not need to be declared with :nil => true
|
19
|
+
def project
|
20
|
+
Dummy.new('project')
|
21
|
+
end
|
22
|
+
|
23
|
+
# This method can return nil and must be declared with :nil => true
|
24
|
+
def spouse
|
25
|
+
nil
|
26
|
+
end
|
27
|
+
|
28
|
+
def husband
|
29
|
+
nil
|
30
|
+
end
|
31
|
+
|
32
|
+
def zip
|
33
|
+
10
|
34
|
+
end
|
35
|
+
end
|
data/test/test_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rubyless
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Gaspard Bucher
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-06-02 00:00:00 +02:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description:
|
17
|
+
email: gaspard@teti.ch
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- README.rdoc
|
24
|
+
files:
|
25
|
+
- History.txt
|
26
|
+
- Rakefile
|
27
|
+
- README.rdoc
|
28
|
+
- rubyless.gemspec
|
29
|
+
- test/mock
|
30
|
+
- test/mock/dummy_class.rb
|
31
|
+
- test/RubyLess
|
32
|
+
- test/RubyLess/basic.yml
|
33
|
+
- test/RubyLess/errors.yml
|
34
|
+
- test/RubyLess_test.rb
|
35
|
+
- test/test_helper.rb
|
36
|
+
- lib/RubyLess.rb
|
37
|
+
- lib/SafeClass.rb
|
38
|
+
has_rdoc: true
|
39
|
+
homepage: http://zenadmin.org/546
|
40
|
+
post_install_message:
|
41
|
+
rdoc_options:
|
42
|
+
- --main
|
43
|
+
- README.rdoc
|
44
|
+
require_paths:
|
45
|
+
- lib
|
46
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
47
|
+
requirements:
|
48
|
+
- - ">="
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: "0"
|
51
|
+
version:
|
52
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: "0"
|
57
|
+
version:
|
58
|
+
requirements: []
|
59
|
+
|
60
|
+
rubyforge_project: rubyless
|
61
|
+
rubygems_version: 1.3.1
|
62
|
+
signing_key:
|
63
|
+
specification_version: 2
|
64
|
+
summary: RubyLess is an interpreter for "safe ruby". The idea is to transform some "unsafe" ruby code into safe, type checked ruby, eventually rewriting some variables or methods
|
65
|
+
test_files: []
|
66
|
+
|