wwmd 0.2.20.3

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.
Files changed (80) hide show
  1. data/History.txt +38 -0
  2. data/README.rdoc +87 -0
  3. data/Rakefile +33 -0
  4. data/examples/config_example.yaml +24 -0
  5. data/examples/wwmd_example.rb +73 -0
  6. data/lib/wwmd.rb +84 -0
  7. data/lib/wwmd/class_extensions.rb +4 -0
  8. data/lib/wwmd/class_extensions/extensions_base.rb +251 -0
  9. data/lib/wwmd/class_extensions/extensions_encoding.rb +79 -0
  10. data/lib/wwmd/class_extensions/extensions_external.rb +18 -0
  11. data/lib/wwmd/class_extensions/extensions_nilclass.rb +11 -0
  12. data/lib/wwmd/class_extensions/extensions_rbkb.rb +193 -0
  13. data/lib/wwmd/class_extensions/mixins_string_encoding.rb +40 -0
  14. data/lib/wwmd/guid.rb +155 -0
  15. data/lib/wwmd/page.rb +3 -0
  16. data/lib/wwmd/page/_fa.old +302 -0
  17. data/lib/wwmd/page/auth.rb +17 -0
  18. data/lib/wwmd/page/constants.rb +63 -0
  19. data/lib/wwmd/page/form.rb +99 -0
  20. data/lib/wwmd/page/form_array.rb +304 -0
  21. data/lib/wwmd/page/headers.rb +118 -0
  22. data/lib/wwmd/page/helpers.rb +41 -0
  23. data/lib/wwmd/page/html2text_hpricot.rb +76 -0
  24. data/lib/wwmd/page/html2text_nokogiri.rb +42 -0
  25. data/lib/wwmd/page/inputs.rb +47 -0
  26. data/lib/wwmd/page/irb_helpers.rb +114 -0
  27. data/lib/wwmd/page/page.rb +257 -0
  28. data/lib/wwmd/page/parsing_convenience.rb +98 -0
  29. data/lib/wwmd/page/reporting_helpers.rb +89 -0
  30. data/lib/wwmd/page/scrape.rb +196 -0
  31. data/lib/wwmd/page/spider.rb +127 -0
  32. data/lib/wwmd/urlparse.rb +125 -0
  33. data/lib/wwmd/viewstate.rb +17 -0
  34. data/lib/wwmd/viewstate/viewstate.rb +101 -0
  35. data/lib/wwmd/viewstate/viewstate_deserializer_methods.rb +217 -0
  36. data/lib/wwmd/viewstate/viewstate_from_xml.rb +129 -0
  37. data/lib/wwmd/viewstate/viewstate_types.rb +51 -0
  38. data/lib/wwmd/viewstate/viewstate_utils.rb +164 -0
  39. data/lib/wwmd/viewstate/viewstate_yaml.rb +25 -0
  40. data/lib/wwmd/viewstate/vs_stubs.rb +22 -0
  41. data/lib/wwmd/viewstate/vs_stubs/vs_array.rb +38 -0
  42. data/lib/wwmd/viewstate/vs_stubs/vs_binary_serialized.rb +30 -0
  43. data/lib/wwmd/viewstate/vs_stubs/vs_hashtable.rb +42 -0
  44. data/lib/wwmd/viewstate/vs_stubs/vs_hybrid_dict.rb +42 -0
  45. data/lib/wwmd/viewstate/vs_stubs/vs_indexed_string.rb +6 -0
  46. data/lib/wwmd/viewstate/vs_stubs/vs_indexed_string_ref.rb +24 -0
  47. data/lib/wwmd/viewstate/vs_stubs/vs_int_enum.rb +27 -0
  48. data/lib/wwmd/viewstate/vs_stubs/vs_list.rb +34 -0
  49. data/lib/wwmd/viewstate/vs_stubs/vs_pair.rb +29 -0
  50. data/lib/wwmd/viewstate/vs_stubs/vs_read_types.rb +11 -0
  51. data/lib/wwmd/viewstate/vs_stubs/vs_read_value.rb +35 -0
  52. data/lib/wwmd/viewstate/vs_stubs/vs_sparse_array.rb +58 -0
  53. data/lib/wwmd/viewstate/vs_stubs/vs_string.rb +33 -0
  54. data/lib/wwmd/viewstate/vs_stubs/vs_string_array.rb +39 -0
  55. data/lib/wwmd/viewstate/vs_stubs/vs_string_formatted.rb +32 -0
  56. data/lib/wwmd/viewstate/vs_stubs/vs_stub_helpers.rb +37 -0
  57. data/lib/wwmd/viewstate/vs_stubs/vs_triplet.rb +31 -0
  58. data/lib/wwmd/viewstate/vs_stubs/vs_type.rb +23 -0
  59. data/lib/wwmd/viewstate/vs_stubs/vs_unit.rb +30 -0
  60. data/lib/wwmd/viewstate/vs_stubs/vs_value.rb +35 -0
  61. data/lib/wwmd/wwmd_config.rb +52 -0
  62. data/lib/wwmd/wwmd_puts.rb +9 -0
  63. data/lib/wwmd/wwmd_utils.rb +28 -0
  64. data/spec/README +3 -0
  65. data/spec/form_array.spec +49 -0
  66. data/spec/spider_csrf_test.spec +28 -0
  67. data/spec/urlparse_test.spec +101 -0
  68. data/tasks/ann.rake +80 -0
  69. data/tasks/bones.rake +20 -0
  70. data/tasks/gem.rake +201 -0
  71. data/tasks/git.rake +40 -0
  72. data/tasks/notes.rake +27 -0
  73. data/tasks/post_load.rake +34 -0
  74. data/tasks/rdoc.rake +51 -0
  75. data/tasks/rubyforge.rake +55 -0
  76. data/tasks/setup.rb +292 -0
  77. data/tasks/spec.rake +54 -0
  78. data/tasks/test.rake +40 -0
  79. data/tasks/zentest.rake +36 -0
  80. metadata +222 -0
@@ -0,0 +1,129 @@
1
+ module WWMD
2
+ class ViewState
3
+ # directly serialize into stack from received xml (the easy way)
4
+ # this is pretty silly but it didn't take me very long so...
5
+
6
+ attr_reader :xmlstack
7
+
8
+ def get_sym(str)
9
+ str.split(":").last.gsub(/[A-Z]+/,'\1_\0').downcase[1..-1].gsub(/\Avs/,"").to_sym
10
+ end
11
+
12
+ def opcode(name,val)
13
+ sym = get_sym(name)
14
+ if sym == :value
15
+ ret = VIEWSTATE_TYPES.index(val.to_sym)
16
+ else
17
+ ret = VIEWSTATE_TYPES.index(sym)
18
+ end
19
+ ret
20
+ end
21
+
22
+ def serialize_hashtable(node)
23
+ tstack = ""
24
+ tstack << self.write_7bit_encoded_int(node['size'].to_i)
25
+ node.children.each do |c|
26
+ next if c.text?
27
+ raise "Invalid Hashtable: got #{c.name}" if not c.name == "Pair"
28
+ end
29
+ tstack
30
+ end
31
+
32
+ def decode_text(node)
33
+ case node['encoding']
34
+ when "urlencoded"
35
+ node.inner_text.unescape
36
+ when "quoted-printable"
37
+ node.inner_text.from_qp
38
+ when "base64"
39
+ node.inner_text.b64d
40
+ when "hexify"
41
+ node.inner_text.dehexify
42
+ else
43
+ # node.inner_text
44
+ node.inner_text.unescape # ZZZZ auto-unescape for fuzzing
45
+ end
46
+ end
47
+
48
+ def write_node(node)
49
+ return false if node.text?
50
+ tstack = ""
51
+ # this is a hack to get sparse_array to work
52
+ return false if ["Pair","Key","Value"].include?(node.name) # skip and fall through
53
+ if ["Index","Size","Elements"].include?(node.name)
54
+ @xmlstack << self.write_7bit_encoded_int(node.inner_text.to_i)
55
+ return false
56
+ end
57
+ if node.name == "Mac"
58
+ @xmlstack << decode_text(node)
59
+ return false
60
+ end
61
+ # end hack
62
+ flag = true # begin; sillyness; rescue => me; end
63
+ case get_sym(node.name)
64
+ when :pair, :triplet, :value, :sparse_array, :type, :string_formatted
65
+ when :int_enum, :known_color, :int32
66
+ tstack << self.write_7bit_encoded_int(node.inner_text.to_i)
67
+ when :int16
68
+ tstack << self.write_short(node.inner_text.to_i)
69
+ when :byte, :char, :indexed_string_ref
70
+ tstack << self.write_byte(node.inner_text.to_i)
71
+ when :color, :single
72
+ tstack << self.write_int32(node.inner_text.to_i)
73
+ when :double, :date_time
74
+ tstack << self.write_double(node.inner_text.to_i)
75
+ when :unit
76
+ tstack << self.write_double(node['dword'].to_i)
77
+ tstack << self.write_single(node['word'].to_i)
78
+ when :list, :string_array, :array
79
+ tstack << self.write_7bit_encoded_int(node['size'].to_i)
80
+ when :string, :indexed_string, :binary_serialized
81
+ flag = false if ([:string_array,:string_formatted].include?(get_sym(node.parent.name)))
82
+ # get encoding
83
+ str = decode_text(node)
84
+ tstack << self.write_7bit_encoded_int(str.size)
85
+ tstack << str
86
+ when :hashtable, :hybrid_dict
87
+ tstack << serialize_hashtable(node)
88
+ else
89
+ raise "Invalid Node:\n#{node.name}"
90
+ end
91
+
92
+ # [flag] is a hack to get around string_array and string_formatted emitting opcodes
93
+ @xmlstack << self.write_byte(opcode(node.name,node.inner_text)) if flag
94
+ if node.has_attribute?("typeref")
95
+ if node['typeref'].to_i == 0x2b
96
+ @xmlstack << self.serialize_type(node['typeref'].to_i,node['typeval'].to_i)
97
+ else
98
+ @xmlstack << self.serialize_type(node['typeref'].to_i,node['typeval'])
99
+ end
100
+ end
101
+ @xmlstack << tstack
102
+ end
103
+
104
+ def serialize_xml(node)
105
+ begin
106
+ write_node(node)
107
+ rescue => e
108
+ STDERR.puts "ERROR parsing node:\n#{node.to_s}"
109
+ raise e
110
+ end
111
+ node.children.each do |c|
112
+ serialize_xml(c)
113
+ end
114
+ end
115
+
116
+ def from_xml(xml)
117
+ @xmlstack = ""
118
+ doc = Nokogiri::XML.parse(xml)
119
+ root = doc.root
120
+ raise "Invalid ViewState Version" if not root.has_attribute?("version")
121
+ @xmlstack << root['version'].b64d
122
+ root.children.each do |c|
123
+ serialize_xml(c)
124
+ end
125
+ self.deserialize(@xmlstack.b64e)
126
+ end
127
+
128
+ end
129
+ end
@@ -0,0 +1,51 @@
1
+ module WWMD
2
+ # private const byte Marker_Format = 0xff;
3
+ # private const byte Marker_Version_1 = 1;
4
+ # private const int StringTableSize = 0xff;
5
+
6
+ VIEWSTATE_MAGIC = ["\xff\x01"] unless defined?(VIEWSTATE_MAGIC)
7
+
8
+ VIEWSTATE_TYPES = {
9
+ # System.Web.UI.LosFormatter
10
+ # System.Web.UI.ObjectStateFormatter
11
+ # .DeserializeValue
12
+
13
+ 0x00 => :debug, ##X debugging
14
+ 0x01 => :int16, #RX private const byte Token_Int16 = 1;
15
+ 0x02 => :int32, #RX private const byte Token_Int32 = 2;
16
+ 0x03 => :byte, #RX private const byte Token_Byte = 3;
17
+ 0x04 => :char, #RX private const byte Token_Char = 4;
18
+ 0x05 => :string, ##X private const byte Token_String = 5;
19
+ 0x06 => :date_time, #RX private const byte Token_DateTime = 6;
20
+ 0x07 => :double, #RX private const byte Token_Double = 7;
21
+ 0x08 => :single, #RX private const byte Token_Single = 8;
22
+ 0x09 => :color, ##X private const byte Token_Color = 9;
23
+ 0x0a => :known_color, ##X private const byte Token_KnownColor = 10;
24
+ 0x0b => :int_enum, ##X private const byte Token_IntEnum = 11;
25
+ 0x0c => :empty_color, #VX private const byte Token_EmptyColor = 12;
26
+ 0x0f => :pair, ##X private const byte Token_Pair = 15;
27
+ 0x10 => :triplet, ##X private const byte Token_Triplet = 0x10;
28
+ 0x14 => :array, ##X private const byte Token_Array = 20;
29
+ 0x15 => :string_array, ##X private const byte Token_StringArray = 0x15;
30
+ 0x16 => :list, ##X private const byte Token_ArrayList = 0x16;
31
+ 0x17 => :hashtable, ##X private const byte Token_Hashtable = 0x17
32
+ 0x18 => :hybrid_dict, ##X private const byte Token_HybridDictionary = 0x18;
33
+ 0x19 => :type, ##X private const byte Token_Type = 0x19;
34
+ 0x1b => :unit, ##X private const byte Token_Unit = 0x1b;
35
+ 0x1c => :empty_unit, #VX private const byte Token_EmptyUnit = 0x1c;
36
+ 0x1e => :indexed_string, ##X private const byte Token_IndexedStringAdd = 30;
37
+ 0x1f => :indexed_string_ref, ##X private const byte Token_IndexedString = 0x1f;
38
+ 0x28 => :string_formatted, ##X private const byte Token_StringFormatted = 40;
39
+ 0x29 => :typeref_add, ##X private const byte Token_TypeRefAdd = 0x29;
40
+ 0x2a => :typeref_add_local, ##X private const byte Token_TypeRefAddLocal = 0x2a;
41
+ 0x2b => :typeref, ##X private const byte Token_TypeRef = 0x2b;
42
+ 0x32 => :binary_serialized, ##X private const byte Token_BinarySerialized = 50;
43
+ 0x3c => :sparse_array, ##X private const byte Token_SparseArray = 60;
44
+ 0x64 => :null, #VX private const byte Token_Null = 100;
45
+ 0x65 => :empty_byte, #VX private const byte Token_EmptyString = 0x65;
46
+ 0x66 => :zeroint32, #VX private const byte Token_ZeroInt32 = 0x66;
47
+ 0x67 => :bool_true, #VX private const byte Token_True = 0x67;
48
+ 0x68 => :bool_false, #VX private const byte Token_False = 0x68;
49
+ } unless defined?(VIEWSTATE_TYPES)
50
+
51
+ end
@@ -0,0 +1,164 @@
1
+ module WWMD
2
+ module ViewStateUtils
3
+
4
+ def putd(msg)
5
+ puts(msg) if self.debug
6
+ end
7
+
8
+ def slog(obj,msg=nil)
9
+ raise "No @value" if not obj.respond_to?(:value)
10
+ raise "No @size" if not obj.respond_to?(:size)
11
+ return nil if !self.debug
12
+ putd "#{@stack.size.to_s(16).rjust(8,"0")} [0x#{obj.opcode.to_s(16)}] #{obj.class}: #{msg}"
13
+ end
14
+
15
+ def dlog(t,msg)
16
+ raise "null token passed to dlog()" if t.nil?
17
+ o = WWMD::VIEWSTATE_TYPES[t]
18
+ @obj_counts[o] ||= 0
19
+ @obj_counts[o] += 1
20
+ return nil if !self.debug
21
+ putd "#{self.last_offset} [0x#{t.to_s(16).rjust(2,"0")}] #{VIEWSTATE_TYPES[t]}: #{msg}"
22
+ end
23
+
24
+ def write_7bit_encoded_int(val)
25
+ s = ""
26
+ while (val >= 0x80) do
27
+ s << [val | 0x80].pack("C")
28
+ val = val >> 7
29
+ end
30
+ s << [val].pack("C")
31
+ return s
32
+ end
33
+
34
+ # why oh why did I have to go find this?
35
+ # System.IO.BinaryReader.Read7BitEncodedInt
36
+ def read_7bit_encoded_int(buf=nil)
37
+ l = 0 # length
38
+ s = 0 # shift
39
+ b = "" # byte
40
+ buf = buf.scan(/./m) if buf
41
+ begin
42
+ if not buf
43
+ b = self.read_int
44
+ else
45
+ b = buf.shift.unpack("C").first
46
+ end
47
+ l |= (b & 0x7f) << s
48
+ s += 7
49
+ end while ((b & 0x80) != 0)
50
+ return l
51
+ end
52
+
53
+ def read_string
54
+ len = read_7bit_encoded_int
55
+ starr = []
56
+ (1..len).each { |i| starr << @buf.read(1) }
57
+ return starr.to_s
58
+ end
59
+
60
+ def read(count)
61
+ @buf.read(count)
62
+ end
63
+
64
+ def read_int
65
+ @buf.read(1).unpack("C").first
66
+ end
67
+ alias_method :read_byte, :read_int
68
+
69
+ def write_int(val)
70
+ [val].pack("C")
71
+ end
72
+ alias_method :write_byte, :write_int
73
+
74
+ def read_short
75
+ self.read(2).unpack("S").first
76
+ end
77
+
78
+ def write_short(val)
79
+ [val].pack("n")
80
+ end
81
+
82
+ def read_int32
83
+ @buf.read(4).unpack("L").first
84
+ end
85
+ alias_method :read_single, :read_int32
86
+
87
+ def write_int32(val)
88
+ [val].pack("I")
89
+ end
90
+ alias_method :write_single, :write_int32
91
+
92
+ def read_double
93
+ @buf.read(8).unpack("Q").first
94
+ end
95
+
96
+ def write_double(val)
97
+ [val].pack("Q")
98
+ end
99
+
100
+ def magic?
101
+ @magic = [@buf.read(1),@buf.read(1)].join("")
102
+ VIEWSTATE_MAGIC.include?(@magic)
103
+ end
104
+
105
+ def read_raw_byte
106
+ @buf.read
107
+ end
108
+
109
+ def serialize_type(op,ref)
110
+ op_str = [op].pack("C")
111
+ s = op_str
112
+ case op
113
+ when VIEWSTATE_TYPES.index(:typeref)
114
+ s << write_7bit_encoded_int(ref)
115
+ when VIEWSTATE_TYPES.index(:typeref_add_local)
116
+ s << write_7bit_encoded_int(ref.size)
117
+ s << ref
118
+ when VIEWSTATE_TYPES.index(:typeref_add)
119
+ s << write_7bit_encoded_int(ref.size)
120
+ s << ref
121
+ else
122
+ raise "Invalid Type Error #{op.to_s(16)}"
123
+ end
124
+ return s
125
+ end
126
+
127
+ def deserialize_type(t=nil)
128
+ op = self.read_byte
129
+ case op
130
+ when VIEWSTATE_TYPES.index(:typeref)
131
+ type = read_7bit_encoded_int
132
+ return [op,type]
133
+ when VIEWSTATE_TYPES.index(:typeref_add_local)
134
+ name = read_string
135
+ return [op,name]
136
+ when VIEWSTATE_TYPES.index(:typeref_add)
137
+ name = read_string
138
+ return [op,name]
139
+ else
140
+ raise "Invalid Type Error 0x#{op.to_s(16)}"
141
+ end
142
+ end
143
+
144
+ def offset(cur=nil)
145
+ @buf.pos.to_s(16).rjust(8,"0")
146
+ end
147
+
148
+ def throw(t=nil)
149
+ puts t.class
150
+ STDERR.puts "==== Error at Type 0x#{t.to_s(16).rjust(2,"0")}"
151
+ STDERR.puts "offset: #{self.offset}"
152
+ STDERR.puts "left: #{@buf.size}"
153
+ STDERR.puts @buf.read(32).hexdump
154
+ end
155
+
156
+ def next_type
157
+ b = @buf.pos
158
+ t = read_byte
159
+ @buf.pos = b
160
+ throw(t) if not VIEWSTATE_TYPES.include?(t)
161
+ VIEWSTATE_TYPES[t]
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,25 @@
1
+ class String
2
+ # right now I have no idea why "\x0d\x0a" is getting munged in yaml transforms
3
+ # something weird helped find by timur@. double up "\r" before "\n" works
4
+ # this might be mac specific and break on other platforms. I don't care.
5
+ # patch not for general use do not try this at home.
6
+ def to_yaml( opts = {} )
7
+ YAML::quick_emit( is_complex_yaml? ? object_id : nil, opts ) do |out|
8
+ if is_binary_data?
9
+ out.scalar( "tag:yaml.org,2002:binary", [self].pack("m"), :literal )
10
+ elsif ( self =~ /\r\n/ )
11
+ # out.scalar( "tag:yaml.org,2002:binary", [self].pack("m"), :literal )
12
+ out.scalar( taguri, self.gsub(/\r\n/,"\r\r\n"), :quote2 )
13
+ elsif to_yaml_properties.empty?
14
+ out.scalar( taguri, self, self =~ /^:/ ? :quote2 : to_yaml_style )
15
+ else
16
+ out.map( taguri, to_yaml_style ) do |map|
17
+ map.add( 'str', "#{self}" )
18
+ to_yaml_properties.each do |m|
19
+ map.add( m, instance_variable_get( m ) )
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,22 @@
1
+ module WWMD::VSStubs; end
2
+ require 'wwmd/viewstate/vs_stubs/vs_stub_helpers'
3
+ require 'wwmd/viewstate/vs_stubs/vs_read_value'
4
+ require 'wwmd/viewstate/vs_stubs/vs_read_types'
5
+ require 'wwmd/viewstate/vs_stubs/vs_value'
6
+ require 'wwmd/viewstate/vs_stubs/vs_array'
7
+ require 'wwmd/viewstate/vs_stubs/vs_binary_serialized'
8
+ require 'wwmd/viewstate/vs_stubs/vs_int_enum'
9
+ require 'wwmd/viewstate/vs_stubs/vs_hashtable'
10
+ require 'wwmd/viewstate/vs_stubs/vs_hybrid_dict'
11
+ require 'wwmd/viewstate/vs_stubs/vs_list'
12
+ require 'wwmd/viewstate/vs_stubs/vs_pair'
13
+ require 'wwmd/viewstate/vs_stubs/vs_sparse_array'
14
+ require 'wwmd/viewstate/vs_stubs/vs_string'
15
+ require 'wwmd/viewstate/vs_stubs/vs_string_array'
16
+ require 'wwmd/viewstate/vs_stubs/vs_string_formatted'
17
+ require 'wwmd/viewstate/vs_stubs/vs_triplet'
18
+ require 'wwmd/viewstate/vs_stubs/vs_type'
19
+ require 'wwmd/viewstate/vs_stubs/vs_unit'
20
+ require 'wwmd/viewstate/vs_stubs/vs_indexed_string'
21
+ require 'wwmd/viewstate/vs_stubs/vs_indexed_string_ref'
22
+
@@ -0,0 +1,38 @@
1
+ module WWMD
2
+ class VSStubs::VSArray
3
+ include VSStubHelpers
4
+
5
+ attr_accessor :value
6
+ attr_reader :typeref
7
+ attr_reader :typeval
8
+
9
+ def initialize(typeref,typeval)
10
+ @typeref = typeref
11
+ @typeval = typeval
12
+ @value = []
13
+ end
14
+
15
+ def add(obj)
16
+ @value << obj
17
+ end
18
+
19
+ def serialize
20
+ stack = super
21
+ stack << self.write_7bit_encoded_int(self.value.size)
22
+ self.value.each do |v|
23
+ stack << v.serialize
24
+ end
25
+ return stack
26
+ end
27
+
28
+ def to_xml
29
+ xml = super
30
+ xml.add_attribute("size", self.value.size.to_s)
31
+ self.value.each do |v|
32
+ xml.add_element(v.to_xml)
33
+ end
34
+ xml
35
+ end
36
+
37
+ end
38
+ end
@@ -0,0 +1,30 @@
1
+ module WWMD
2
+ class VSStubs::VSBinarySerialized
3
+ include VSStubHelpers
4
+
5
+ attr_accessor :value
6
+
7
+ def initialize()
8
+ @value = ''
9
+ end
10
+
11
+ def set(str)
12
+ @value = str
13
+ end
14
+
15
+ def serialize
16
+ stack = super
17
+ stack << self.write_7bit_encoded_int(self.size)
18
+ stack << self.value
19
+ return stack
20
+ end
21
+
22
+ def to_xml
23
+ xml = super
24
+ xml.add_attribute("encoding","base64")
25
+ xml.add_text(self.value.b64e)
26
+ xml
27
+ end
28
+
29
+ end
30
+ end