model_security_generator 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/README ADDED
File without changes
data/USAGE ADDED
File without changes
@@ -0,0 +1,75 @@
1
+ class ModelSecurityGenerator < Rails::Generator::NamedBase
2
+ def manifest
3
+ record do |m|
4
+ # Check for class naming collisions.
5
+ m.class_collisions class_path, "User", "UserController", "UserMailer", "UserSupport", "Modal", "ModalHelper", "ModelSecurity", "ModelSecurityHelper"
6
+
7
+ # Libraries
8
+ m.file "modal.rb", "lib/modal.rb"
9
+ m.file "model_security.rb", "lib/model_security.rb"
10
+ m.file "once.rb", "lib/once.rb"
11
+ m.file "user_support.rb", "lib/user_support.rb"
12
+
13
+ # Helpers
14
+ m.file "modal_helper.rb", "app/helpers/modal_helper.rb"
15
+ m.file "model_security_helper.rb", "app/helpers/model_security_helper.rb"
16
+
17
+ # User
18
+ m.file "user.rb", File.join("app/models", "user.rb")
19
+ m.file "user_controller.rb", File.join("app/controllers", "user_controller.rb")
20
+
21
+ # User mailer
22
+ m.file "user_mailer.rb", File.join("app/models", "user_mailer.rb")
23
+
24
+ # Testing related stuff
25
+ m.file "user_controller_test.rb", "test/functional/user_controller_test.rb"
26
+ m.file "user_test.rb", "test/unit/user_test.rb"
27
+ m.file "mock_mailer.rb", "test/mocks/test/user_mailer.rb"
28
+ m.file "mock_time.rb", "test/mocks/test/time.rb"
29
+ m.file "users.yml", "test/fixtures/users.yml"
30
+
31
+ # Schemas, configuration and miscellaneous
32
+ m.file "schema.sql", "db/schema.sql"
33
+ m.file "users.sql", "db/users.sql"
34
+
35
+ # Layout and stylesheet.
36
+ m.file "scaffold.rhtml", "app/views/layouts/scaffold.rhtml"
37
+ m.file "scaffold.css", "public/stylesheets/scaffold.css"
38
+ m.file "standard.rhtml", "app/views/layouts/standard.rhtml"
39
+ m.file "standard.css", "public/stylesheets/standard.css"
40
+
41
+ # Views
42
+ m.directory File.join("app/views", "user", file_name)
43
+ user_views.each do |action|
44
+ m.file "view_#{action}.rhtml",
45
+ File.join("app/views", "user", file_name, "#{action}.rhtml")
46
+ end
47
+
48
+ # Partials
49
+ m.directory File.join("app/views", "user", file_name)
50
+ partial_views.each do |action|
51
+ m.file "_view_#{action}.rhtml",
52
+ File.join("app/views", "user", file_name, "_#{action}.rhtml")
53
+ end
54
+
55
+ # Mailer
56
+ m.directory File.join("app/views", "user_mailer")
57
+ mailer_views.each do |action|
58
+ m.file "mailer_#{action}.rhtml",
59
+ File.join("app/views", "user_mailer", "#{action}.rhtml")
60
+ end
61
+ end
62
+ end
63
+
64
+ def user_views
65
+ %w(activate edit forgot_password_done list login login_admin logout new show success)
66
+ end
67
+
68
+ def partial_views
69
+ %w(form)
70
+ end
71
+
72
+ def mailer_views
73
+ %w(forgot_password new_user)
74
+ end
75
+ end
@@ -0,0 +1,27 @@
1
+ <%= error_messages_for 'user' %>
2
+
3
+ <!--[form:user]-->
4
+ <table>
5
+ <% for column in User.content_columns %>
6
+ <% if User.display?(column.name) %>
7
+ <tr>
8
+ <th>
9
+ <label for="user_#{column.name}">
10
+ <%= column.human_name %>:
11
+ </label>
12
+ </th>
13
+ <td>
14
+ <% if column.name.index('password') %>
15
+ <%= password_field('user', column.name) %>
16
+ <% else %>
17
+ <%= text_field('user', column.name) %>
18
+ <% end %>
19
+ </td>
20
+ </tr>
21
+ <% end %>
22
+ <% end %>
23
+
24
+ </table>
25
+
26
+ <!--[eoform:user]-->
27
+
@@ -0,0 +1,18 @@
1
+ <h1>Recover Your Login or Re-Set Your Password</h1>
2
+
3
+ <p>
4
+ Please enter the email address you used when you registered your login.
5
+ A message will be sent to that email with the information you'll need to
6
+ reset your password.
7
+ </p>
8
+ <%= start_form_tag :action => 'forgot_password' %>
9
+ <table>
10
+ <tr>
11
+ <th><label for="user_email">Email</label></th>
12
+ <td><%= text_field 'user', 'email' %></td>
13
+ </tr>
14
+ </table>
15
+ <%= submit_tag "Send Email" %>
16
+ <%= end_form_tag %>
17
+
18
+ <%= link_to 'Back', :action => 'list' %>
@@ -0,0 +1,10 @@
1
+ Dear <%= @user.name %>,
2
+
3
+ Your login '<%= @user.login %>' has been created, but you must now activate it.
4
+ To do so, please access the following URL before <%= @user.token_expiry %>:
5
+
6
+ <%= @url %>
7
+
8
+ Many Thanks
9
+
10
+ The Management
@@ -0,0 +1,16 @@
1
+ require 'models/user_mailer.rb'
2
+
3
+ ActionMailer::Base.class_eval {
4
+ @@inject_one_error = false
5
+ cattr_accessor :inject_one_error
6
+
7
+ private
8
+ def perform_delivery_test(mail)
9
+ if inject_one_error
10
+ ActionMailer::Base::inject_one_error = false
11
+ raise "Failed to send email" if raise_delivery_errors
12
+ else
13
+ deliveries << mail
14
+ end
15
+ end
16
+ }
@@ -0,0 +1,17 @@
1
+ require 'time'
2
+
3
+ Time.class_eval {
4
+ @@advance_by_days = 0
5
+ cattr_accessor :advance_by_days
6
+
7
+ class << Time
8
+ alias now_old now
9
+ def now
10
+ if Time.advance_by_days != 0
11
+ return Time.at(now_old.to_i + Time.advance_by_days * 60 * 60 * 24 + 1)
12
+ else
13
+ now_old
14
+ end
15
+ end
16
+ end
17
+ }
@@ -0,0 +1,82 @@
1
+ # Implement modal web pages. When you are done with one of these pages, they
2
+ # will redirect you to an internal page anchor within the page that linked to
3
+ # them. In other words, you don't just go back to the referring page. You go
4
+ # back to the place within the referring page where the link was.
5
+ #
6
+ # This implementation is in two parts: the library module Modal, and the view
7
+ # helper ModalHelper.
8
+ #
9
+ module Modal
10
+
11
+ # This is meant to be called as a before filter by the ApplicationController.
12
+ # It always returns true and thus will not interrupt your action.
13
+ #
14
+ # The link_modal and modal_form methods generate an internal page
15
+ # anchor and a +ret+ parameter that will be passed in a GET or PUT.
16
+ # The anchor refers the location within the calling page to return to
17
+ # after a modal action, and +ret+ encodes the URL of that anchor.
18
+ # store_return retrieves +ret+ from the request and saves it in a
19
+ # return-to address URL in the session. The return-to URL will be used
20
+ # by redirect_back_or_default to return to a location within the calling
21
+ # page that linked to the current page. This is nicer than simply returning
22
+ # to the top of the page, especially when returning to a long page like
23
+ # a list of blog comments, because the reader will probably want to continue
24
+ # reading at that point.
25
+ #
26
+ # If the request does not contain a +ret+ parameter and the return-to
27
+ # address is not set and REFERER is set in the environment, the
28
+ # return-to address is set to REFERER.
29
+ #
30
+ def modal_setup
31
+ # Set modal return.
32
+ if r = @params['ret']
33
+ self.return_location = r.sub(/\.L/, '#L')
34
+ elsif return_location.nil?
35
+ self.return_location = @request.env['HTTP_REFERER']
36
+ end
37
+ true
38
+ end
39
+
40
+ # Get the location to return to after a modal action. The argument should
41
+ # be a string containing a URL.
42
+ def return_location
43
+ @session[:return_to]
44
+ end
45
+
46
+ # Set the location to return to after a modal action. The return value should
47
+ # be a string containing a URL.
48
+ def return_location= a
49
+ @session[:return_to] = a
50
+ end
51
+
52
+ # Store the location to return to after a modal action from the request URI.
53
+ # This is usually called before redirecting to another action.
54
+ def store_location
55
+ self.return_location = @request.request_uri
56
+ end
57
+
58
+ # Redirect to the stored return location. If no stored return location
59
+ # is available, redirect to the default action given in the arguments.
60
+ # The arguments are just like those of redirect_to.
61
+ def redirect_back_or_default(attributes = {}, *method_params)
62
+ r = return_location
63
+ if r
64
+ redirect_to_url r
65
+ self.return_location = nil
66
+ else
67
+ redirect_to attributes, method_params
68
+ end
69
+ end
70
+
71
+ # Create a URL optionally including an internal anchor. If the +id+ argument
72
+ # is given, that will be used to create the name of the internal anchor. It's
73
+ # generally a number, but anything that will convert to a string that does
74
+ # not contain characters that would have special meaning within a URL, like
75
+ # spaces, will do.
76
+ def return_url(id = nil)
77
+ s = @request.request_uri.sub(/\??ret=[^\&$]*/,'')
78
+ s += '.L' + id.to_s if id
79
+ s
80
+ end
81
+
82
+ end
@@ -0,0 +1,29 @@
1
+ require 'modal'
2
+
3
+ module ModalHelper
4
+ include Modal
5
+
6
+ # Include "ret" in a form, for modal forms.
7
+ def modal_form(id = nil)
8
+ s = '<input type="hidden" name="ret" value="' + return_url(id) + '"'
9
+ s += 'id="L' + id.to_s + '"' if id
10
+ s += '/>'
11
+ s
12
+ end
13
+
14
+ # Include "ret" in a link, for modal links.
15
+ def modal_link_to(name, id = nil, options = {}, html_options = nil, *dict_params)
16
+ html_options = {} if not html_options
17
+ html_options['id'] = 'L' + id.to_s if id
18
+ html_options = html_options.stringify_keys
19
+ convert_confirm_option_to_javascript!(html_options)
20
+ if options.is_a?(String)
21
+ l = content_tag "a", name || options, html_options.merge("href" => options)
22
+ else
23
+ s = url_for(options, dict_params)
24
+ s += ('?ret=' + return_url(id))
25
+ l = content_tag("a", (name || url_for(options, dict_params)), html_options.merge("href" => s))
26
+ end
27
+ l
28
+ end
29
+ end
@@ -0,0 +1,334 @@
1
+ # The ModelSecurity module allows you to specify security permissions on any
2
+ # or all of the attributes of a model implemented using ActiveRecord.
3
+ #
4
+ # Security permissions are
5
+ # specified in the declaration of the model's class, similarly to the way
6
+ # you can specify validators. The specification includes the names of
7
+ # attributes to which permissions apply, and an optional permission test that
8
+ # should return true or false depending on whether the access should be allowed
9
+ # or denied.
10
+ #
11
+ # let_read :attribute|:all [[, :attribute ] ...], [:if => :test-name] [do block end]
12
+ # let_write :attribute|:all [[, :attribute ] ...], [:if => :test-name] [do block end]
13
+ # let_access :attribute|:all [[, :attribute ] ...], [:if => :test-name] [do block end]
14
+ #
15
+ # let_read specifies when the attribute can be read, let_write specifies when
16
+ # it can be written, and let_access does both.
17
+ #
18
+ # If no permission test is specified, that is the same as specifying a test
19
+ # that always returns true. Two stub tests are provided:
20
+ #
21
+ # :always?
22
+ #
23
+ # Always returns true.
24
+ #
25
+ # :never?
26
+ #
27
+ # Always returns false.
28
+ #
29
+ # You can easily add your own tests as instance methods of your model:
30
+ #
31
+ # let_read :phone_number :if => :admin?
32
+ #
33
+ # def admin?
34
+ # return $current_login.is_the_administrator
35
+ # end
36
+ #
37
+ # If the permission test is specified using the syntax
38
+ # :if => :test-name
39
+ # it will be run as a method of the model this way:
40
+ # self.send(:test-name)
41
+ #
42
+ # If the permission test is specified as a block, using *do* and *end*,
43
+ # it will be called with the binding of the active record instance that
44
+ # is being accessed.
45
+ #
46
+ # Permission tests can also be strings, and these are passed to eval().
47
+ #
48
+ # The special attribute name :all means that a test will be applied to all
49
+ # attributes of the model. Any tests for :all are run first, then any tests
50
+ # for the specific attribute. Any test that returns true ends the run, further
51
+ # tests will not be evaluated.
52
+ #
53
+ # If *no* security permissions are declared for an attribute or :all, that
54
+ # attribute may always be accessed. Once a test for :all is delcared, that
55
+ # test will apply to all attributes of the model.
56
+ #
57
+ # The security tests themselves may access any data with impunity. A global
58
+ # variable is used to disable further security testing while a security test
59
+ # is in progress.
60
+ #
61
+ # = Display Control
62
+ #
63
+ # A companion mechanism is used to control views, including scaffold views,
64
+ # using a syntax similar to that for security specifications:
65
+ #
66
+ # let_display :attribute|:all [[, :attribute ] ...], [:if => :test-name] [do block end]
67
+ #
68
+ # let_display is mostly useful for specifying if a table view should have a
69
+ # column for a particular attribute. Its tests must be declared as class
70
+ # methods of the model, while the tests of let_read, let_write, and
71
+ # let_access are instance methods. This is because the information declared
72
+ # by let_display is accessed before iteration over active records begins.
73
+ #
74
+ #
75
+ # = Accessing Security Test Results
76
+ #
77
+ # ModelSecurity provides two instance methods, readable? and writable?
78
+ # to inform the program if a particular attribute can be accessed. The class
79
+ # method display? will return true or false depending upon whether a
80
+ # particular attribute should be displayed. These can
81
+ # be used to modify a view so that any non-writable data will not be presented
82
+ # in an editable field. ModelSecurityHelper overloads the methods that are
83
+ # usually
84
+ # used to edit models so that they will not attempt to read or write what they
85
+ # aren't permitted and will render appropriately for the permissions on any
86
+ # model attribute. Those methods are: check_box, file_field, hidden_field,
87
+ # password_field, radio_button, text_area, text_field.
88
+ # ModelSecurityHelper also replaces the scaffold views with versions that
89
+ # never access data when not permitted to, render appropriately for the
90
+ # permissions on an attribute, and omit columns for which display? returns
91
+ # false.
92
+ #
93
+ #
94
+ # = Exceptions
95
+ #
96
+ # ActiveRecord provides two internal methods that perform normal attribute
97
+ # accesses: read_attribute, and write_attribute. These are overloaded to
98
+ # perform security testing, and will raise *SecurityError* when an unpermitted
99
+ # access is attempted.
100
+ #
101
+ module ModelSecurity
102
+ end
103
+
104
+ require 'once'
105
+
106
+ module ModelSecurity::BothClassAndInstanceMethods
107
+ private
108
+ # Run a single test.
109
+ def run_test t
110
+ case t.class.name
111
+ when 'Proc'
112
+ return t.call(binding)
113
+ when 'Symbol'
114
+ return self.send(t)
115
+ when 'String'
116
+ return eval(t)
117
+ else
118
+ return false
119
+ end
120
+ end
121
+
122
+ # This does the permission test for readable?, writable?, and display?.
123
+ # A global variable is used to short-circuit recursion.
124
+ #
125
+ # FIX: The global variable should be replaced with a thread-local variable
126
+ # once I learn how to make one.
127
+ #
128
+ def run_tests(d, attribute)
129
+ global = d[:all]
130
+ local = d[attribute.to_sym]
131
+ result = true
132
+
133
+ if (global or local) and ($in_test_permission != true)
134
+ $in_test_permission = true
135
+ result = (run_test(global) or run_test(local))
136
+ $in_test_permission = false
137
+ end
138
+ return result
139
+ end
140
+
141
+ public
142
+ # Stub test for let_read and friends. Always returns true.
143
+ def always?
144
+ true
145
+ end
146
+
147
+ # Stub test for let_read and friends. Always returns false.
148
+ def never?
149
+ false
150
+ end
151
+ end
152
+
153
+ module ModelSecurity
154
+ include ModelSecurity::BothClassAndInstanceMethods
155
+
156
+ private
157
+ # This does the permission test for readable? or writable?.
158
+ def test_permission(permission, attribute)
159
+ if (d = self.class.read_inheritable_attribute(permission))
160
+ return run_tests(d, attribute)
161
+ else
162
+ return true
163
+ end
164
+ end
165
+
166
+ # Responsible for raising an exception when an unpermitted security
167
+ # access is attempted. *permission* is :let_read or :let_write.
168
+ # *attribute* is the name of the attribute upon which an access is
169
+ # being attempted.
170
+ #
171
+ # FIX: Is exception information displayed in production mode? I put a lot
172
+ # of sensitive data in this exception.
173
+ #
174
+ def security_error(permission, attribute)
175
+ global = nil
176
+ local = nil
177
+
178
+ if (d = self.class.read_inheritable_attribute(permission))
179
+ global = d[:all]
180
+ local = d[attribute.to_sym]
181
+ end
182
+
183
+ message = "SECURITY VIOLATON: #{permission} on attribute #{attribute}" \
184
+ "\n\tof object: #{self.inspect}."
185
+
186
+ if global
187
+ message << "\n\tTest for :all is #{global.inspect}."
188
+ end
189
+
190
+ if local
191
+ message << "\n\tTest for :#{attribute} is #{local.inspect}."
192
+ end
193
+
194
+ raise SecurityError.new(message)
195
+ end
196
+
197
+ public
198
+ # Ruby interpreter magic to cause the class methods herein to work correctly
199
+ # while a class including this module is still being declared.
200
+ def self.append_features(base)
201
+ super
202
+ base.extend(ClassMethods)
203
+ end
204
+
205
+ # Return true if a read of *attribute* is permitted.
206
+ # *attribute* should be a symbol, and should be the
207
+ # name of a database field for this model.
208
+ def readable?(attribute)
209
+ test_permission(:let_read, attribute)
210
+ end
211
+
212
+ # Overloads ActiveRecord::Base#read_attribute. Read the attribute if that is
213
+ # permitted. Otherwise, throw an exception.
214
+ def read_attribute(name)
215
+ if not readable?(name)
216
+ security_error(:let_read, name)
217
+ end
218
+ old_read_attribute(name)
219
+ end
220
+
221
+ # Return true if a write of *attribute* is permitted.
222
+ # *attribute* should be a symbol, and should be the
223
+ # name of a database field for this model.
224
+ def writable?(attribute)
225
+ test_permission(:let_write, attribute)
226
+ end
227
+
228
+ # Overloads ActiveRecord::Base#write_attribute. Write the attribute if that is
229
+ # permitted. Otherwise, throw an exception.
230
+ def write_attribute(name, value)
231
+ if not writable?(name)
232
+ security_error(:let_write, name)
233
+ raise SecurityError
234
+ end
235
+ old_write_attribute(name, value)
236
+ end
237
+ end
238
+
239
+ # Class methods for ModelSecurity. They are broken out this way so that they
240
+ # can be fed to base.extend(), Ruby interpreter magic so that class methods
241
+ # of this module work while a class including it is being defined.
242
+ # Uses a Rails-internal inheritable-attribute mechanism so that this data in a
243
+ # derived class survives modification of similar data in its base class.
244
+ #
245
+ # I'm not sure that write_inheritable_attribute() is the right way to go, either
246
+ # here or in the way that validators are declared. Why not declare the data
247
+ # independently in each subclass and then use *super* to traverse the
248
+ # inheritance graph when accessing it?
249
+ #
250
+ module ModelSecurity::ClassMethods
251
+ private
252
+ include ModelSecurity::BothClassAndInstanceMethods
253
+
254
+ # Internal function where the work of let_read, let_write, let_access,
255
+ # and let_display is done. Store the tests to be run for each attribute
256
+ # in the class, to be evaluated later. *permission* is :let_read,
257
+ # :let_write, or :let_display. *arguments* is a list of attributes
258
+ # upon which security permissions are being declared and a hash
259
+ # containing all options, currently just :if . *block*, if given,
260
+ # contains a security test.
261
+ #
262
+ def let(permission, arguments, block)
263
+ attributes = [] # List of attributes that this permission applies to.
264
+ parameters = {} # Optional parameters, currently only :if
265
+ procedure = nil # Permission-test procedure.
266
+
267
+ arguments.each { | argument |
268
+ case argument.class.name
269
+ when 'Hash'
270
+ parameters.merge! argument
271
+ else
272
+ attributes << argument
273
+ end
274
+ }
275
+ if not block.nil?
276
+ procedure = block
277
+ elsif (p = parameters[:if])
278
+ procedure = p
279
+ else
280
+ procedure = :always?
281
+ end
282
+
283
+ d = {}
284
+ attributes.each { | name | d[name] = procedure }
285
+ write_inheritable_hash(permission, d)
286
+ end
287
+
288
+ public
289
+ # Return true if display of the attribute is permitted. *attribute* is a
290
+ # symbol, and should match a field in the database schema corresponding to
291
+ # this model.
292
+ def display?(attribute)
293
+ if (d = read_inheritable_attribute(:let_display))
294
+ return run_tests(d, attribute)
295
+ else
296
+ return true
297
+ end
298
+ end
299
+
300
+ # Declare whether reads and writes are permitted on the named attributes.
301
+ def let_access(*arguments, &block)
302
+ let(:let_read, arguments, block)
303
+ let(:let_write, arguments, block)
304
+ end
305
+
306
+ # Declare whether display of the named attribute is permitted.
307
+ def let_display(*arguments, &block)
308
+ let(:let_display, arguments, block)
309
+ end
310
+
311
+
312
+ # Declare whether read is permitted upon the named attributes.
313
+ def let_read(*arguments, &block)
314
+ let(:let_read, arguments, block)
315
+ end
316
+
317
+ # Declare whether write is permitted upon the named attributes.
318
+ def let_write(*arguments, &block)
319
+ let(:let_write, arguments, block)
320
+ end
321
+ end
322
+
323
+ class ActiveRecord::Base
324
+ private
325
+ once ('@aliasesDone') {
326
+ # Provides access to the pre-overload version of read_attribute, which
327
+ # is called by the overloaded version.
328
+ alias old_read_attribute read_attribute
329
+
330
+ # Provides access to the pre-overload version of write_attribute, which
331
+ # is called by the overloaded version.
332
+ alias old_write_attribute write_attribute
333
+ }
334
+ end
@@ -0,0 +1,64 @@
1
+ module ActionView::Helpers::FormHelper
2
+ private
3
+
4
+ def self.restrict(name)
5
+ module_eval <<-"end_eval", __FILE__, __LINE__
6
+ alias old_#{name} #{name}
7
+ def #{name}(object, method, options = {})
8
+ restricted_input(:old_#{name}, object, method, options) \
9
+ end
10
+ end_eval
11
+ end
12
+
13
+ def restricted_input(old_name, object, method, options, *extra, &block)
14
+ o = instance_variable_get("@" + object)
15
+ writable = o.writable?(method)
16
+ if o.readable?(method)
17
+ if writable
18
+ if block
19
+ return yield(object, method, options, extra)
20
+ else
21
+ return send(old_name, object, method, options)
22
+ end
23
+ else
24
+ return o.send(method)
25
+ end
26
+ elseif writable
27
+ options[:value] = ''
28
+ if block
29
+ return yield(object, method, options, extra)
30
+ else
31
+ return send(old_name, object, method, options)
32
+ end
33
+ end
34
+ end
35
+
36
+ once ('@aliasesDone') { # Evaluating this twice would break it.
37
+ alias old_check_box check_box
38
+ alias old_radio_button radio_button
39
+ }
40
+
41
+ SimpleMethods = \
42
+ ['file_field', 'hidden_field', 'password_field', 'text_area', 'text_field']
43
+
44
+ public
45
+
46
+ once ('@simpleMethodsDone') { # because it calls alias.
47
+ SimpleMethods.each { | name | restrict(name) }
48
+ }
49
+
50
+ def check_box(object, method, options = {}, checked_value = "1", unchecked_value = "0")
51
+ restricted_input(:old_check_box, object, method, options, checked_value, unchecked_value) {
52
+ | object, method, options, extra |
53
+ old_check_box(object, method, options, extra[0], extra[1])
54
+ }
55
+ end
56
+
57
+ def radio_button(object, method, tag_value, options = {})
58
+ restricted_input(:old_radio_button, object, method, options, tag_value) {
59
+ | object, method, options, extra |
60
+ old_radio_button(object, method, extra[0], options)
61
+ }
62
+ end
63
+ end
64
+