ruckus 0.5.4
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +22 -0
- data/README +0 -0
- data/README.markdown +51 -0
- data/Rakefile +54 -0
- data/VERSION +1 -0
- data/lib/ruckus.rb +70 -0
- data/lib/ruckus/blob.rb +113 -0
- data/lib/ruckus/choice.rb +55 -0
- data/lib/ruckus/dfuzz.rb +231 -0
- data/lib/ruckus/dictionary.rb +119 -0
- data/lib/ruckus/enum.rb +39 -0
- data/lib/ruckus/extensions/array.rb +33 -0
- data/lib/ruckus/extensions/class.rb +168 -0
- data/lib/ruckus/extensions/duplicable.rb +37 -0
- data/lib/ruckus/extensions/file.rb +24 -0
- data/lib/ruckus/extensions/fixnum.rb +26 -0
- data/lib/ruckus/extensions/hash.rb +10 -0
- data/lib/ruckus/extensions/integer.rb +26 -0
- data/lib/ruckus/extensions/ipaddr.rb +155 -0
- data/lib/ruckus/extensions/irb.rb +30 -0
- data/lib/ruckus/extensions/math.rb +6 -0
- data/lib/ruckus/extensions/module.rb +37 -0
- data/lib/ruckus/extensions/numeric.rb +117 -0
- data/lib/ruckus/extensions/object.rb +22 -0
- data/lib/ruckus/extensions/range.rb +16 -0
- data/lib/ruckus/extensions/socket.rb +20 -0
- data/lib/ruckus/extensions/string.rb +327 -0
- data/lib/ruckus/filter.rb +16 -0
- data/lib/ruckus/human_display.rb +79 -0
- data/lib/ruckus/ip.rb +38 -0
- data/lib/ruckus/mac_addr.rb +31 -0
- data/lib/ruckus/mutator.rb +318 -0
- data/lib/ruckus/null.rb +24 -0
- data/lib/ruckus/number.rb +360 -0
- data/lib/ruckus/parsel.rb +363 -0
- data/lib/ruckus/selector.rb +92 -0
- data/lib/ruckus/str.rb +164 -0
- data/lib/ruckus/structure.rb +263 -0
- data/lib/ruckus/structure/atcreate.rb +23 -0
- data/lib/ruckus/structure/beforebacks.rb +28 -0
- data/lib/ruckus/structure/defaults.rb +57 -0
- data/lib/ruckus/structure/factory.rb +42 -0
- data/lib/ruckus/structure/fixupfields.rb +16 -0
- data/lib/ruckus/structure/initializers.rb +14 -0
- data/lib/ruckus/structure/relate.rb +34 -0
- data/lib/ruckus/structure/replacement.rb +30 -0
- data/lib/ruckus/structure/searchmods.rb +33 -0
- data/lib/ruckus/structure/structureproxies.rb +28 -0
- data/lib/ruckus/structure/validate.rb +12 -0
- data/lib/ruckus/structure_shortcuts.rb +109 -0
- data/lib/ruckus/time_t.rb +26 -0
- data/lib/ruckus/vector.rb +97 -0
- data/ruckus.gemspec +104 -0
- data/test/test_decides.rb +61 -0
- data/test/test_mutator.rb +29 -0
- data/test/test_override.rb +23 -0
- data/test/test_replace.rb +39 -0
- metadata +138 -0
@@ -0,0 +1,28 @@
|
|
1
|
+
module Ruckus
|
2
|
+
module StructureBeforeCallbacks
|
3
|
+
def self.included(klass)
|
4
|
+
klass.extend(ClassMethods)
|
5
|
+
|
6
|
+
klass.class_eval {
|
7
|
+
class_inheritable_array :before_callbacks
|
8
|
+
write_inheritable_array :before_callbacks, []
|
9
|
+
}
|
10
|
+
end
|
11
|
+
|
12
|
+
def before_render_hook
|
13
|
+
self.class.before_callbacks.each {|cb| self.instance_eval(&cb)}
|
14
|
+
end
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
def before_render(arg=nil, &block)
|
18
|
+
if not block_given?
|
19
|
+
raise "need a callback function" if not arg
|
20
|
+
arg = arg.intern if not arg.kind_of? Symbol
|
21
|
+
block = lambda { send(arg) }
|
22
|
+
end
|
23
|
+
|
24
|
+
self.before_callbacks << block
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Ruckus
|
2
|
+
module StructureDefaultValues
|
3
|
+
def self.included(klass)
|
4
|
+
klass.extend(ClassMethods)
|
5
|
+
end
|
6
|
+
|
7
|
+
def final_initialization_hook
|
8
|
+
instance_variables.grep(/^@with_.*/).each do |v|
|
9
|
+
v =~ /@with_(.*)/
|
10
|
+
var = $1
|
11
|
+
send((var + "=").intern, instance_variable_get(v.intern))
|
12
|
+
end
|
13
|
+
super
|
14
|
+
end
|
15
|
+
|
16
|
+
# XXX probably not needed
|
17
|
+
# def template_entry_added_hook(obj)
|
18
|
+
# obj.instance_variables.grep(/^@with_.*/).each do |v|
|
19
|
+
# v =~ /@with_(.*)/
|
20
|
+
# var = $1
|
21
|
+
# obj.send((var + "=").intern, obj.instance_variable_get(v.intern))
|
22
|
+
# end
|
23
|
+
# super
|
24
|
+
# end
|
25
|
+
|
26
|
+
def method_missing_hook(meth, args)
|
27
|
+
m = meth.to_s
|
28
|
+
setter = (m[-1].chr == "=") ? true : false
|
29
|
+
m = m[0..-2] if setter
|
30
|
+
if setter and (field = self[m.intern])
|
31
|
+
if(field.kind_of? Structure)
|
32
|
+
if args[0].kind_of? field.class
|
33
|
+
args[0].each_field do |name, f|
|
34
|
+
field.value = f.value
|
35
|
+
end
|
36
|
+
else
|
37
|
+
if((deft = field.instance_variable_get :@default_field))
|
38
|
+
field.send(deft.to_s + "=", args[0])
|
39
|
+
return false
|
40
|
+
else
|
41
|
+
puts "WARNING: attempt to set structure field with no default_field declared"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
true
|
47
|
+
end
|
48
|
+
|
49
|
+
module ClassMethods
|
50
|
+
def default_field(f)
|
51
|
+
self.initializers << lambda do
|
52
|
+
@default_field = f
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Ruckus
|
2
|
+
module StructureDetectFactory
|
3
|
+
def factory?
|
4
|
+
self.respond_to? :factory
|
5
|
+
end
|
6
|
+
|
7
|
+
def structure_field_def_hook(*a)
|
8
|
+
args = a[0]
|
9
|
+
opts = args[0].respond_to?(:has_key?) ? args[0] : args[1]
|
10
|
+
include StructureFactory if opts.try(:has_key?, :decides)
|
11
|
+
super
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
module StructureFactory
|
16
|
+
def self.included(klass)
|
17
|
+
klass.extend(ClassMethods)
|
18
|
+
end
|
19
|
+
|
20
|
+
module ClassMethods
|
21
|
+
def factory(str)
|
22
|
+
orig = str.clone
|
23
|
+
(tmp = self.new).capture(str)
|
24
|
+
tmp.each_field do |n, f|
|
25
|
+
if (m = f.try(:decides))
|
26
|
+
klass = m[f.value]
|
27
|
+
if klass
|
28
|
+
o = derive_search_module.const_get(klass.to_s.class_name).new
|
29
|
+
if o.class.factory?
|
30
|
+
o, orig = o.class.factory(orig)
|
31
|
+
else
|
32
|
+
orig = o.capture(orig)
|
33
|
+
end
|
34
|
+
return o, orig
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
return false
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Ruckus
|
2
|
+
module StructureFixupFieldNames
|
3
|
+
def structure_field_def_hook(*a)
|
4
|
+
args = a[0]
|
5
|
+
if args[0] and args[0].kind_of? Symbol
|
6
|
+
if args[1]
|
7
|
+
args[1][:name] = args[0]
|
8
|
+
else
|
9
|
+
args[1] = { :name => args[0] }
|
10
|
+
end
|
11
|
+
args.shift
|
12
|
+
end
|
13
|
+
super
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Ruckus
|
2
|
+
module StructureInitializers
|
3
|
+
def self.included(klass)
|
4
|
+
klass.class_eval {
|
5
|
+
class_inheritable_array :initializers
|
6
|
+
write_inheritable_array :initializers, []
|
7
|
+
}
|
8
|
+
end
|
9
|
+
|
10
|
+
def final_initialization_hook
|
11
|
+
self.initializers.each {|cb| self.instance_eval(&cb) }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Ruckus
|
2
|
+
module StructureRelateDeclaration
|
3
|
+
def class_method_missing_hook(meth, *args)
|
4
|
+
if meth.to_s =~ /^relate_(.*)/
|
5
|
+
relate($1.intern, *args)
|
6
|
+
return false
|
7
|
+
end
|
8
|
+
true
|
9
|
+
end
|
10
|
+
|
11
|
+
def relate(attr, field, opts={})
|
12
|
+
opts[:through] ||= :value
|
13
|
+
raise "need a valid field to relate" if not field
|
14
|
+
raise "need :to argument" if not opts[:to]
|
15
|
+
|
16
|
+
self.initializers << lambda do
|
17
|
+
f = send(field)
|
18
|
+
|
19
|
+
case attr
|
20
|
+
when :value
|
21
|
+
f.value = opts[:through]
|
22
|
+
f.instance_eval { @from_field = opts[:to] }
|
23
|
+
when :size
|
24
|
+
f.instance_eval {
|
25
|
+
@in_size = {
|
26
|
+
:meth => opts[:through],
|
27
|
+
:from_field => opts[:to]
|
28
|
+
}
|
29
|
+
}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Ruckus
|
2
|
+
module StructureAllowFieldReplacement
|
3
|
+
def template_entry_added_hook(obj)
|
4
|
+
## -------------------------------------------
|
5
|
+
## a quick dance to allow fields to replace other
|
6
|
+
## fields
|
7
|
+
|
8
|
+
# check to see if it already exists, in which
|
9
|
+
# case we want to replace the previous definition
|
10
|
+
found = false
|
11
|
+
@value.each_with_index do |x,i|
|
12
|
+
if x.try(:name) == obj.try(:name)
|
13
|
+
found = i
|
14
|
+
break
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# it didn't exist, so add it to the structure
|
19
|
+
if found
|
20
|
+
# it did exist, so update the names index
|
21
|
+
# and the previous entry in the structure
|
22
|
+
|
23
|
+
self.class.structure_field_names[obj.try(:name)] = found
|
24
|
+
@value[found] = obj
|
25
|
+
else
|
26
|
+
false
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Ruckus
|
2
|
+
module StructureSearchModules
|
3
|
+
def self.included(klass)
|
4
|
+
klass.class_eval {
|
5
|
+
class_inheritable_array :search_modules
|
6
|
+
write_inheritable_array :search_modules, []
|
7
|
+
}
|
8
|
+
|
9
|
+
klass.extend(ClassMethods)
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
def derive_search_module
|
14
|
+
if self.search_modules.empty?
|
15
|
+
return Ruckus
|
16
|
+
else
|
17
|
+
mod = Module.new
|
18
|
+
self.search_modules.each do |m|
|
19
|
+
mod.module_eval "include #{ m.to_s.class_name }"
|
20
|
+
end
|
21
|
+
mod.module_eval "include Ruckus"
|
22
|
+
return mod
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def search_module(*args)
|
27
|
+
args.each do |m|
|
28
|
+
self.search_modules << m
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Ruckus
|
2
|
+
module StructureProxies
|
3
|
+
class ValueProxy
|
4
|
+
def initialize(target); @t = target; end
|
5
|
+
def method_missing(meth, *args)
|
6
|
+
if meth.to_s.ends_with? "="
|
7
|
+
@t.send(meth, *args)
|
8
|
+
else
|
9
|
+
@t.send(meth).send(:value)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class NodeProxy
|
15
|
+
def initialize(target); @t = target; end
|
16
|
+
def method_missing(meth, *args)
|
17
|
+
if meth.to_s.ends_with? "="
|
18
|
+
@t.send(meth, *args)
|
19
|
+
else
|
20
|
+
@t[meth]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def v; ValueProxy.new(self); end
|
26
|
+
def n; NodeProxy.new(self); end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'pp'
|
2
|
+
|
3
|
+
module Ruckus
|
4
|
+
module StructureValidateField
|
5
|
+
def structure_field_def_hook(*a)
|
6
|
+
args = a[0]
|
7
|
+
if [ "value", "name", "size" ].include?(nm = args[0][:name] && args[0][:name].to_s) or (nm and nm.starts_with? "relate_")
|
8
|
+
raise "can't have fields named #{ nm }, because we suck; rename the field"
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# === A sort of half-baked DSL for defining packet fields.
|
2
|
+
# These are all classmethods of Structure.
|
3
|
+
module Ruckus
|
4
|
+
class Structure
|
5
|
+
# Any number, ie
|
6
|
+
# num :len, :width => 32, :endianness => :little
|
7
|
+
#
|
8
|
+
def self.num(name, opts)
|
9
|
+
add(Number, opts.merge(:name => name))
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.char(*args); with_args(*args) {|name, opts| str opts.merge(:name => name, :size => 1)}; end
|
13
|
+
|
14
|
+
def self.decimal(*args); with_args(*args) {|name, opts| num name, opts.merge(:ascii => true, :radix => 10)}; end
|
15
|
+
def self.hex_number(*args); with_args(*args) {|name, opts| num name, opts.merge(:ascii => true, :radix => 16)}; end
|
16
|
+
|
17
|
+
def self.tag_bit(*args); with_args(*args) {|name, opts| num name, opts.merge(:width => 1, :tag => name)}; end
|
18
|
+
|
19
|
+
# A string (ie, multiple of 8 bits wide) containing all zeroes.
|
20
|
+
# You could also just use
|
21
|
+
# num :width => whatever, :value => 0
|
22
|
+
#
|
23
|
+
def self.zero_pad(*args)
|
24
|
+
with_args(*args) do |name, opts|
|
25
|
+
str opts.merge(:name => name, :padding => "\x00")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# A bounded string, takes its size from the preceding element
|
30
|
+
#
|
31
|
+
def self.bounded(*args)
|
32
|
+
with_args(*args) do |name, opts|
|
33
|
+
opts[:size] ||= :value
|
34
|
+
opts[:offset] ||= -1
|
35
|
+
str name, opts
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# A string.
|
40
|
+
def self.string(*args)
|
41
|
+
with_args(*args) do |name, opts|
|
42
|
+
str opts.merge(:name => name)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# A Null byte
|
47
|
+
def self.mark(*args)
|
48
|
+
with_args(*args) do |name, opts|
|
49
|
+
null opts.merge(:name => name)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# 4-byte IP address (IPv4)
|
54
|
+
def self.ipv4(*args)
|
55
|
+
with_args(*args) do |name, opts|
|
56
|
+
add(Ruckus::IP, opts.merge(:name => name))
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.choose(name, tag=nil, &block)
|
61
|
+
add(Ruckus::Choice, :name => name, :block => block)
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.base_pad(name, tag=nil)
|
65
|
+
string name, :value => {:offset => :this, :block => lambda do |this|
|
66
|
+
r = this.root
|
67
|
+
r = r.find_tag_struct(tag) if tag
|
68
|
+
if ((k = this.rendered_offset - r.rendered_offset) % 4) != 0
|
69
|
+
pad = 4 - ((this.rendered_offset - r.rendered_offset) % 4)
|
70
|
+
else
|
71
|
+
pad = 0
|
72
|
+
end
|
73
|
+
"\x00" * pad
|
74
|
+
end
|
75
|
+
}
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.word_len(*args); with_args(*args) do |name, opts|
|
79
|
+
opts[:value] ||= :size
|
80
|
+
opts[:width] ||= 16
|
81
|
+
opts[:modifier] = lambda {|o, s| s/=2}
|
82
|
+
num name, opts
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.msg_len(*args); with_args(*args) do |name, opts|
|
87
|
+
opts[:value] = { :block => lambda do |n|
|
88
|
+
if not n.rendering
|
89
|
+
begin
|
90
|
+
n.rendering = true
|
91
|
+
n.parent_struct.size
|
92
|
+
ensure
|
93
|
+
n.rendering = false
|
94
|
+
end
|
95
|
+
else
|
96
|
+
4
|
97
|
+
end
|
98
|
+
end
|
99
|
+
}
|
100
|
+
opts[:width] ||= 32
|
101
|
+
num name, opts
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
class Padding < Structure
|
107
|
+
base_pad :pad
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
|
2
|
+
module Ruckus
|
3
|
+
class TimeT < Number
|
4
|
+
def initialize(opts={})
|
5
|
+
opts[:width] ||= 32
|
6
|
+
super(opts)
|
7
|
+
end
|
8
|
+
|
9
|
+
def to_human(indent="")
|
10
|
+
"#{indent}#{@name} = #{@value.to_hex} (#{time})"
|
11
|
+
end
|
12
|
+
|
13
|
+
def utc(*args)
|
14
|
+
@value=( Time.utc(*args) ).to_i
|
15
|
+
end
|
16
|
+
alias :gm :utc
|
17
|
+
|
18
|
+
def local(*args)
|
19
|
+
@value=( t=Time.local(*args)).to_i
|
20
|
+
end
|
21
|
+
|
22
|
+
def time
|
23
|
+
Time.at(@value)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
module Ruckus
|
2
|
+
|
3
|
+
# A vector of count elements of the same class, as in
|
4
|
+
# [elt0] [elt1] ... [eltN]
|
5
|
+
class Vector < Parsel
|
6
|
+
|
7
|
+
# Options include:
|
8
|
+
# * <tt>:class</tt> the class of each element
|
9
|
+
# * <tt>:e_opts</tt> opts to pass when creating each element (:name is always deleted)
|
10
|
+
# * <tt>:count</tt> the number of elements of class :class in the vector. Can be a reference to another field
|
11
|
+
# via :from_field
|
12
|
+
def initialize(opts={})
|
13
|
+
opts[:count] = 0x1fffffff if opts[:count] == :unlimited # grotesque hack XXX
|
14
|
+
opts[:e_opts] ||= {}
|
15
|
+
|
16
|
+
raise "need a class" if not opts[:class] and not opts[:classes_from]
|
17
|
+
|
18
|
+
if opts[:classes_from]
|
19
|
+
if not opts[:keys_from]
|
20
|
+
raise "need a module to pull keys from (protocol numbers, command IDs, whatever) as :keys_from"
|
21
|
+
end
|
22
|
+
|
23
|
+
begin
|
24
|
+
@keys = (m = opts[:keys_from]).constants.
|
25
|
+
select {|x| m.const_get(x).kind_of? Numeric}.
|
26
|
+
map {|x| [m.const_get(x), x]}.
|
27
|
+
to_hash
|
28
|
+
rescue => e
|
29
|
+
raise "can't look up keys:\n #{ e }"
|
30
|
+
end
|
31
|
+
|
32
|
+
begin
|
33
|
+
@classes = (m = opts[:classes_from]).constants.
|
34
|
+
select {|x| m.const_get(x).kind_of? Class}.
|
35
|
+
map do |x|
|
36
|
+
name = (klass = m.const_get(x)).
|
37
|
+
to_s.
|
38
|
+
underscore.
|
39
|
+
upcase
|
40
|
+
name = name[name.rindex(":")+1..-1]
|
41
|
+
[ name, klass ]
|
42
|
+
end.to_hash
|
43
|
+
rescue => e
|
44
|
+
raise "can't generate class dictionary:\n #{ e }"
|
45
|
+
end
|
46
|
+
|
47
|
+
raise "need a :key_field or :key_finder" if not opts[:key_field] and not opts[:key_finder]
|
48
|
+
end
|
49
|
+
|
50
|
+
super(opts)
|
51
|
+
@value = Blob.new
|
52
|
+
@value.parent = self
|
53
|
+
end
|
54
|
+
|
55
|
+
def capture(str)
|
56
|
+
count = resolve(@count)
|
57
|
+
if not count
|
58
|
+
raise "You need to provide a :count value to parse a vector; did you give it :size by mistake?"
|
59
|
+
end
|
60
|
+
|
61
|
+
count.downto(1) do |i|
|
62
|
+
break if not str or str.empty?
|
63
|
+
|
64
|
+
if @class
|
65
|
+
o = @class.new(@e_opts.merge(:parent => self))
|
66
|
+
str = o.capture(str)
|
67
|
+
@value << o
|
68
|
+
else
|
69
|
+
if @key_field
|
70
|
+
key = parent_struct.send(@key_field)
|
71
|
+
end
|
72
|
+
|
73
|
+
if @key_finder
|
74
|
+
key = @key_finder.call(str)
|
75
|
+
end
|
76
|
+
|
77
|
+
begin
|
78
|
+
o = @classes[@keys[key]].new(@e_opts.merge(:parent => self))
|
79
|
+
str = o.capture(str)
|
80
|
+
@value << o
|
81
|
+
rescue => e
|
82
|
+
raise "couldn't create an object from key:\n#{ e }"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
@count = @value.count
|
87
|
+
str
|
88
|
+
end
|
89
|
+
|
90
|
+
def to_s(off=nil)
|
91
|
+
@rendered_offset = off || 0
|
92
|
+
(@value)? @value.to_s(off) : ""
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|