コードを記述するコード。一般的に言われているメタプログラミングの概念。
メタプログラミングの総集編の章。動的メソッド、カーネルメソッドなどの復習をしつつ、
フックメソッドなどの新しいトリックを理解していきます。
evalの問題点
evalが評価するコード文字列内でもローカル変数にアクセスできる。
そういう意味ではブロックと似ている。
v1 = 1 eval("v1") # => 1
evalとブロックどちらを使うのが良いか。
答えは「ブロック」らしい。
コード文字列(evalを用いた処理記述)は問題点がある。
コードインジェクション
外部からコード文字列を読み込んだ際に、悪意のあるコードを読み込んでしまい、
プライベート情報漏洩やハードディスクの情報を消されたりしてしまうこと
def my_method(method) eval("'a'.#{method}") end # 悪意のあるコード プライベートな情報が表示されてしまう。 my_method(object_id; Dir.glob("*") 'a'.object_id Dir.glob("*")
オブジェクトの汚染とセーフレベル
Rubyは安全でないオブジェクトに汚染マークを付ける。
webフォーム、コマンドライン、システム変数の読み込み文字列など
を含んでいた場合、安全でないと判断
user_input = "User input: #{gets()}" # 汚染されているかどうかをチェック puts user_input.tainted? x = 1 true
汚染マークを検知して、例外を発生させるセーフレベルという仕組みがある
潜在的に危険な操作に対して制限をかけられる。
# セーフレベルを1に設定 user_input = "User input: #{gets()}" eval user_input x = 1 # セーフレベル1以上の場合、汚染した文字列を評価できない SecurityError: Insecure operatiion - eval
セーフレベルについては以下を参照
セキュリティモデル (Ruby 1.8.7)
属性追加メソッドを作成(evalを使用)
コードを記述するコードについて学ぶ為に
attr_accessor()に検証機能を付加したadd_checked_attribute()を作成する。
特徴は以下
・カーネルメソッド
・Ruby(コード文字列)を使用
・シンプルな検証機能
まず、満たしたい機能のテストを書く。
require 'test/unit' class Person; end class TestCheckedAttribute < Test::Unit::TestCase def setup # Personクラスにage属性と取得、設定メソッドを追加 add_checked_attribute(Person, :age) @bob = Person.new end def test_accepts_vaid_value @bob.age = 20 assert_equal 20, @bob.age end def test_refuses_false_value assert_raises RuntimeError, 'Invalid attribute' do @bob.age = false end end def test_refuses_nil_value assert_raises RuntimeError, 'Invalid attribute' do @bob.age = nil end end def add_checked_attribute(clazz, attribute) # この後実装 end end
add_checked_attributeをevalを用いて実装
def add_checked_attribute(clazz, attribute) eval "" # オープンクラスを用いて、指定クラスにメソッド追加 class #{clazz} def #{attribute}=(value) raise 'Invalid attribute' unless value @#{attribute} = value end def #{attribute}() @#{attribute} end end "" end # 上記メソッドを呼び出した場合 add_checked_attribute(Array, my_attr) class Array def my_attr=(value) raise 'Invalid attribute' unless value @my_attr = value end def my_attr() @my_attr end end
属性追加メソッドを作成(evalを使わないで実装)
add_checked_attributeが外部公開されるとすると、 コードインジェクション対策としてevalを使わない実装にリファクタリングする必要がある。
def add_checked_attribute(clazz, attribute) # 任意のクラスのクラススコープにメソッドを定義 clazz.class_eval do # 任意の名称のメソッドを定義するので動的メソッドを使う define_method "#{attribute}=" do |value| raise 'Invalid attribute' unless value # 任意のインスタンス変数に値を設定するのでinstance_variable_setを使う instance_variable_set("@#{attribute}", value) end define_method attribute do # 任意のインスタンス変数の値を取得するのでinstance_variable_setを使う instance_variable_get "@#{attribute}" end end end
属性追加メソッドを作成(属性検証にブロック使用可とする)
ここでは設定値が18以上でないと例外発生する条件をブロック指定する。
上記、仕様追加のため、テストコードを追加
require 'test/unit' class Person; end class TestCheckedAttribute < Test::Unit::TestCase def setup # Personクラスにage属性と取得、設定メソッドを追加 add_checked_attribute(Person, :age) {|v| v >= 18 } @bob = Person.new end def test_refuses_values assert_raises RuntimeError, 'Invalid attribute' do @bob.age = 17 end end def add_checked_attribute(clazz, attribute, &validation) clazz.class_eval do define_method "#{attribute}=" do |value| raise 'Invalid attribute' unless validation.call(value) instance_variable_set("@#{attribute}", value) end define_method attribute do instance_variable_get "@#{attribute}" end end end end
属性追加メソッドを作成(カーネルメソッドをクラスマクロへ)
require 'test/unit' class Person # クラスマクロとして呼び出す attr_checked :age do |v| v >= 18 end end class TestCheckedAttribute < Test::Unit::TestCase def setup @bob = Person.new end def test_refuses_values assert_raises RuntimeError, 'Invalid attribute' do @bob.age = 17 end end end class Class # Classのインスタンスメソッドとして定義。Personクラスではクラスメソッドになる def attr_checked(attribute, &validation) # Class自身に実装されるのでclass_eval不要になった define_method "#{attribute}=" do |value| raise 'Invalid attribute' unless validation.call(value) instance_variable_set("@#{attribute}", value) end define_method attribute do instance_variable_get "@#{attribute}" end end end
フックメソッド
さまざまなイベントを契機に処理を行うことができる。(継承、拡張など)
module MyModule # includedがフックメソッド def self.included(othermod) # MyModuleが拡張されると下記が処理される puts "MyModuleは #{othermod}にmixinされた" end end class C include MyModule end # => MymoduleはCにmixinされた
属性追加メソッドを作成(モジュール拡張時に自動でメソッド追加)
CheckedAttributesをインクルードしたクラスのみが、
attr_checkedにアクセスできるように修正する。
require 'test/unit' class Person include CheckedAttributes attr_checked :age do |v| v >= 18 end end ・・・
CheckedAttributesモジュールを実装する。
クラス拡張ミックスインのトリックを使う。include時にクラス拡張を行う。
module CheckedAttributes # included呼び出し先クラス(base)のクラスメソッドとしてattr_checkedを定義 def self.included(base) base.extend ClassMethods end module ClassMethods def attr_checked(attribute, &validation) ・・・
第5章完了