bit-struct 0.13.1
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/.gitignore +2 -0
- data/History.txt +102 -0
- data/README.txt +189 -0
- data/Rakefile +34 -0
- data/TODO +23 -0
- data/TODO-ALSO +71 -0
- data/examples/ara-player-data.rb +82 -0
- data/examples/bignum.rb +18 -0
- data/examples/bits.rb +19 -0
- data/examples/byte-bdy.rb +30 -0
- data/examples/field-ripper.rb +22 -0
- data/examples/fixed-point.rb +17 -0
- data/examples/ip.rb +81 -0
- data/examples/longlong.rb +30 -0
- data/examples/md.rb +23 -0
- data/examples/modular-def.rb +38 -0
- data/examples/native.rb +31 -0
- data/examples/nested.rb +33 -0
- data/examples/pad.rb +14 -0
- data/examples/ping-recv.rb +25 -0
- data/examples/ping.rb +73 -0
- data/examples/player-data.rb +75 -0
- data/examples/raw.rb +62 -0
- data/examples/rest.rb +30 -0
- data/examples/switch-endian.rb +49 -0
- data/examples/vector.rb +98 -0
- data/lib/bit-struct.rb +6 -0
- data/lib/bit-struct/bit-struct.rb +549 -0
- data/lib/bit-struct/char-field.rb +48 -0
- data/lib/bit-struct/fields.rb +273 -0
- data/lib/bit-struct/float-field.rb +61 -0
- data/lib/bit-struct/hex-octet-field.rb +20 -0
- data/lib/bit-struct/nested-field.rb +76 -0
- data/lib/bit-struct/octet-field.rb +45 -0
- data/lib/bit-struct/pad-field.rb +15 -0
- data/lib/bit-struct/signed-field.rb +258 -0
- data/lib/bit-struct/text-field.rb +44 -0
- data/lib/bit-struct/unsigned-field.rb +248 -0
- data/lib/bit-struct/vector-field.rb +77 -0
- data/lib/bit-struct/vector.rb +173 -0
- data/lib/bit-struct/yaml.rb +69 -0
- data/tasks/ann.rake +80 -0
- data/tasks/bones.rake +20 -0
- data/tasks/gem.rake +201 -0
- data/tasks/git.rake +40 -0
- data/tasks/notes.rake +27 -0
- data/tasks/post_load.rake +34 -0
- data/tasks/rdoc.rake +51 -0
- data/tasks/rubyforge.rake +55 -0
- data/tasks/setup.rb +292 -0
- data/tasks/spec.rake +54 -0
- data/tasks/svn.rake +47 -0
- data/tasks/test.rake +40 -0
- data/tasks/zentest.rake +36 -0
- data/test/test-endian.rb +39 -0
- data/test/test-vector.rb +38 -0
- data/test/test.rb +433 -0
- metadata +126 -0
@@ -0,0 +1,48 @@
|
|
1
|
+
class BitStruct
|
2
|
+
# Class for fixed length binary strings of characters.
|
3
|
+
# Declared with BitStruct.char.
|
4
|
+
class CharField < Field
|
5
|
+
#def self.default
|
6
|
+
# don't define this, since it must specify N nulls and we don't know N
|
7
|
+
#end
|
8
|
+
|
9
|
+
# Used in describe.
|
10
|
+
def self.class_name
|
11
|
+
@class_name ||= "char"
|
12
|
+
end
|
13
|
+
|
14
|
+
def add_accessors_to(cl, attr = name) # :nodoc:
|
15
|
+
unless offset % 8 == 0
|
16
|
+
raise ArgumentError,
|
17
|
+
"Bad offset, #{offset}, for #{self.class} #{name}." +
|
18
|
+
" Must be multiple of 8."
|
19
|
+
end
|
20
|
+
|
21
|
+
unless length % 8 == 0
|
22
|
+
raise ArgumentError,
|
23
|
+
"Bad length, #{length}, for #{self.class} #{name}." +
|
24
|
+
" Must be multiple of 8."
|
25
|
+
end
|
26
|
+
|
27
|
+
offset_byte = offset / 8
|
28
|
+
length_byte = length / 8
|
29
|
+
last_byte = offset_byte + length_byte - 1
|
30
|
+
byte_range = offset_byte..last_byte
|
31
|
+
val_byte_range = 0..length_byte-1
|
32
|
+
|
33
|
+
cl.class_eval do
|
34
|
+
define_method attr do ||
|
35
|
+
self[byte_range].to_s
|
36
|
+
end
|
37
|
+
|
38
|
+
define_method "#{attr}=" do |val|
|
39
|
+
val = val.to_s
|
40
|
+
if val.length < length_byte
|
41
|
+
val += "\0" * (length_byte - val.length)
|
42
|
+
end
|
43
|
+
self[byte_range] = val[val_byte_range]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,273 @@
|
|
1
|
+
class BitStruct
|
2
|
+
class << self
|
3
|
+
# Define a char string field in the current subclass of BitStruct,
|
4
|
+
# with the given _name_ and _length_ (in bits). Trailing nulls _are_
|
5
|
+
# considered part of the string.
|
6
|
+
#
|
7
|
+
# If a class is provided, use it for the Field class.
|
8
|
+
# If a string is provided, use it for the display_name.
|
9
|
+
# If a hash is provided, use it for options.
|
10
|
+
#
|
11
|
+
# Note that the accessors have COPY semantics, not reference.
|
12
|
+
#
|
13
|
+
def char(name, length, *rest)
|
14
|
+
opts = parse_options(rest, name, CharField)
|
15
|
+
add_field(name, length, opts)
|
16
|
+
end
|
17
|
+
alias string char
|
18
|
+
autoload :CharField, "bit-struct/char-field"
|
19
|
+
|
20
|
+
# Define a floating point field in the current subclass of BitStruct,
|
21
|
+
# with the given _name_ and _length_ (in bits).
|
22
|
+
#
|
23
|
+
# If a class is provided, use it for the Field class.
|
24
|
+
# If a string is provided, use it for the display_name.
|
25
|
+
# If a hash is provided, use it for options.
|
26
|
+
#
|
27
|
+
# The <tt>:endian => :native</tt> option overrides the default of
|
28
|
+
# <tt>:network</tt> byte ordering, in favor of native byte ordering. Also
|
29
|
+
# permitted are <tt>:big</tt> (same as <tt>:network</tt>) and
|
30
|
+
# <tt>:little</tt>.
|
31
|
+
#
|
32
|
+
def float name, length, *rest
|
33
|
+
opts = parse_options(rest, name, FloatField)
|
34
|
+
add_field(name, length, opts)
|
35
|
+
end
|
36
|
+
autoload :FloatField, "bit-struct/float-field"
|
37
|
+
|
38
|
+
# Define an octet string field in the current subclass of BitStruct,
|
39
|
+
# with the given _name_ and _length_ (in bits). Trailing nulls are
|
40
|
+
# not considered part of the string. The field is accessed using
|
41
|
+
# period-separated hex digits.
|
42
|
+
#
|
43
|
+
# If a class is provided, use it for the Field class.
|
44
|
+
# If a string is provided, use it for the display_name.
|
45
|
+
# If a hash is provided, use it for options.
|
46
|
+
#
|
47
|
+
def hex_octets(name, length, *rest)
|
48
|
+
opts = parse_options(rest, name, HexOctetField)
|
49
|
+
add_field(name, length, opts)
|
50
|
+
end
|
51
|
+
autoload :HexOctetField, "bit-struct/hex-octet-field"
|
52
|
+
|
53
|
+
# Define a nested field in the current subclass of BitStruct,
|
54
|
+
# with the given _name_ and _nested_class_. Length is determined from
|
55
|
+
# _nested_class_.
|
56
|
+
#
|
57
|
+
# In _rest_:
|
58
|
+
#
|
59
|
+
# If a class is provided, use it for the Field class (i.e. <=NestedField).
|
60
|
+
# If a string is provided, use it for the display_name.
|
61
|
+
# If a hash is provided, use it for options.
|
62
|
+
#
|
63
|
+
# WARNING: the accessors have COPY semantics, not reference. When you call a
|
64
|
+
# reader method to get the nested structure, you get a *copy* of that data.
|
65
|
+
#
|
66
|
+
# For example:
|
67
|
+
#
|
68
|
+
# class Sub < BitStruct
|
69
|
+
# unsigned :x, 8
|
70
|
+
# end
|
71
|
+
#
|
72
|
+
# class A < BitStruct
|
73
|
+
# nest :n, Sub
|
74
|
+
# end
|
75
|
+
#
|
76
|
+
# a = A.new
|
77
|
+
#
|
78
|
+
# p a # ==> #<A n=#<Sub x=0>>
|
79
|
+
#
|
80
|
+
# # This fails to set x in a.
|
81
|
+
# a.n.x = 3
|
82
|
+
# p a # ==> #<A n=#<Sub x=0>>
|
83
|
+
#
|
84
|
+
# # This works
|
85
|
+
# n = a.n
|
86
|
+
# n.x = 3
|
87
|
+
# a.n = n
|
88
|
+
# p a # ==> #<A n=#<Sub x=3>>
|
89
|
+
#
|
90
|
+
def nest(name, nested_class, *rest)
|
91
|
+
opts = parse_options(rest, name, NestedField)
|
92
|
+
opts[:default] ||= nested_class.initial_value.dup
|
93
|
+
opts[:nested_class] = nested_class
|
94
|
+
field = add_field(name, nested_class.bit_length, opts)
|
95
|
+
field
|
96
|
+
end
|
97
|
+
alias struct nest
|
98
|
+
autoload :NestedField, "bit-struct/nested-field"
|
99
|
+
|
100
|
+
# Define an octet string field in the current subclass of BitStruct,
|
101
|
+
# with the given _name_ and _length_ (in bits). Trailing nulls are
|
102
|
+
# not considered part of the string. The field is accessed using
|
103
|
+
# period-separated decimal digits.
|
104
|
+
#
|
105
|
+
# If a class is provided, use it for the Field class.
|
106
|
+
# If a string is provided, use it for the display_name.
|
107
|
+
# If a hash is provided, use it for options.
|
108
|
+
#
|
109
|
+
def octets(name, length, *rest)
|
110
|
+
opts = parse_options(rest, name, OctetField)
|
111
|
+
add_field(name, length, opts)
|
112
|
+
end
|
113
|
+
autoload :OctetField, "bit-struct/octet-field"
|
114
|
+
|
115
|
+
# Define a padding field in the current subclass of BitStruct,
|
116
|
+
# with the given _name_ and _length_ (in bits).
|
117
|
+
#
|
118
|
+
# If a class is provided, use it for the Field class.
|
119
|
+
# If a string is provided, use it for the display_name.
|
120
|
+
# If a hash is provided, use it for options.
|
121
|
+
#
|
122
|
+
def pad(name, length, *rest)
|
123
|
+
opts = parse_options(rest, name, PadField)
|
124
|
+
add_field(name, length, opts)
|
125
|
+
end
|
126
|
+
alias padding pad
|
127
|
+
autoload :PadField, "bit-struct/pad-field"
|
128
|
+
|
129
|
+
# Define a signed integer field in the current subclass of BitStruct,
|
130
|
+
# with the given _name_ and _length_ (in bits).
|
131
|
+
#
|
132
|
+
# If a class is provided, use it for the Field class.
|
133
|
+
# If a string is provided, use it for the display_name.
|
134
|
+
# If a hash is provided, use it for options.
|
135
|
+
#
|
136
|
+
# SignedField adds the <tt>:fixed => divisor</tt> option, which specifies
|
137
|
+
# that the internally stored value is interpreted as a fixed point real
|
138
|
+
# number with the specified +divisor+.
|
139
|
+
#
|
140
|
+
# The <tt>:endian => :native</tt> option overrides the default of
|
141
|
+
# <tt>:network</tt> byte ordering, in favor of native byte ordering. Also
|
142
|
+
# permitted are <tt>:big</tt> (same as <tt>:network</tt>) and
|
143
|
+
# <tt>:little</tt>.
|
144
|
+
#
|
145
|
+
def signed name, length, *rest
|
146
|
+
opts = parse_options(rest, name, SignedField)
|
147
|
+
add_field(name, length, opts)
|
148
|
+
end
|
149
|
+
autoload :SignedField, "bit-struct/signed-field"
|
150
|
+
|
151
|
+
# Define a printable text string field in the current subclass of BitStruct,
|
152
|
+
# with the given _name_ and _length_ (in bits). Trailing nulls are
|
153
|
+
# _not_ considered part of the string.
|
154
|
+
#
|
155
|
+
# If a class is provided, use it for the Field class.
|
156
|
+
# If a string is provided, use it for the display_name.
|
157
|
+
# If a hash is provided, use it for options.
|
158
|
+
#
|
159
|
+
# Note that the accessors have COPY semantics, not reference.
|
160
|
+
#
|
161
|
+
def text(name, length, *rest)
|
162
|
+
opts = parse_options(rest, name, TextField)
|
163
|
+
add_field(name, length, opts)
|
164
|
+
end
|
165
|
+
autoload :TextField, "bit-struct/text-field"
|
166
|
+
|
167
|
+
# Define a unsigned integer field in the current subclass of BitStruct,
|
168
|
+
# with the given _name_ and _length_ (in bits).
|
169
|
+
#
|
170
|
+
# If a class is provided, use it for the Field class.
|
171
|
+
# If a string is provided, use it for the display_name.
|
172
|
+
# If a hash is provided, use it for options.
|
173
|
+
#
|
174
|
+
# UnsignedField adds the <tt>:fixed => divisor</tt> option, which specifies
|
175
|
+
# that the internally stored value is interpreted as a fixed point real
|
176
|
+
# number with the specified +divisor+.
|
177
|
+
#
|
178
|
+
# The <tt>:endian => :native</tt> option overrides the default of
|
179
|
+
# <tt>:network</tt> byte ordering, in favor of native byte ordering. Also
|
180
|
+
# permitted are <tt>:big</tt> (same as <tt>:network</tt>) and
|
181
|
+
# <tt>:little</tt>.
|
182
|
+
#
|
183
|
+
def unsigned name, length, *rest
|
184
|
+
opts = parse_options(rest, name, UnsignedField)
|
185
|
+
add_field(name, length, opts)
|
186
|
+
end
|
187
|
+
autoload :UnsignedField, "bit-struct/unsigned-field"
|
188
|
+
|
189
|
+
# Define a vector field in the current subclass of BitStruct,
|
190
|
+
# with the given _name_.
|
191
|
+
#
|
192
|
+
# If a class is provided, use it for the Vector class, otherwise
|
193
|
+
# the block must define the entry fields. The two forms looks like
|
194
|
+
# this:
|
195
|
+
#
|
196
|
+
# class Vec < BitStruct::Vector
|
197
|
+
# # these declarations apply to *each* entry in the vector:
|
198
|
+
# unsigned :x, 16
|
199
|
+
# signed :y, 32
|
200
|
+
# end
|
201
|
+
#
|
202
|
+
# class Packet < BitStruct
|
203
|
+
# # Using the Vec class defined above
|
204
|
+
# vector :v, Vec, "a vector", :length => 5
|
205
|
+
#
|
206
|
+
# # equivalently, using an anonymous subclass of BitStruct::Vector
|
207
|
+
# vector :v2, "a vector", :length => 5 do
|
208
|
+
# unsigned :x, 16
|
209
|
+
# signed :y, 32
|
210
|
+
# end
|
211
|
+
# end
|
212
|
+
#
|
213
|
+
# If a string is provided, use it for the display_name.
|
214
|
+
# If a hash is provided, use it for options.
|
215
|
+
#
|
216
|
+
# WARNING: the accessors have COPY semantics, not reference. When you call a
|
217
|
+
# reader method to get the vector structure, you get a *copy* of that data.
|
218
|
+
#
|
219
|
+
# For example, to modify the numeric fields in a Packet as defined above:
|
220
|
+
#
|
221
|
+
# pkt = Packet.new
|
222
|
+
# vec = pkt.v
|
223
|
+
# entry = vec[2]
|
224
|
+
# entry.x = 123
|
225
|
+
# entry.y = -456
|
226
|
+
# vec[2] = entry
|
227
|
+
# pkt.v = vec
|
228
|
+
#
|
229
|
+
def vector(name, *rest, &block)
|
230
|
+
opts = parse_options(rest, name, nil)
|
231
|
+
cl = opts[:field_class]
|
232
|
+
opts[:field_class] = VectorField
|
233
|
+
|
234
|
+
unless (block and not cl) or (cl and not block)
|
235
|
+
raise ArgumentError,
|
236
|
+
"vector must have either a class or a block, but not both"
|
237
|
+
end
|
238
|
+
|
239
|
+
case
|
240
|
+
when cl == nil
|
241
|
+
vector_class = Class.new(BitStruct::Vector)
|
242
|
+
vector_class.class_eval(&block)
|
243
|
+
|
244
|
+
when cl < BitStruct
|
245
|
+
vector_class = Class.new(BitStruct::Vector)
|
246
|
+
vector_class.struct_class cl
|
247
|
+
|
248
|
+
when cl < BitStruct::Vector
|
249
|
+
vector_class = cl
|
250
|
+
|
251
|
+
else raise ArgumentError, "Bad vector class: #{cl.inspect}"
|
252
|
+
end
|
253
|
+
|
254
|
+
vector_class.default_options default_options
|
255
|
+
|
256
|
+
length = opts[:length] ## what about :length => :lenfield
|
257
|
+
unless length
|
258
|
+
raise ArgumentError, "Must provide length as :length => N"
|
259
|
+
end
|
260
|
+
|
261
|
+
opts[:default] ||= vector_class.new(length) ## nil if variable length
|
262
|
+
opts[:vector_class] = vector_class
|
263
|
+
|
264
|
+
bit_length = vector_class.struct_class.round_byte_length * 8 * length
|
265
|
+
|
266
|
+
field = add_field(name, bit_length, opts)
|
267
|
+
field
|
268
|
+
end
|
269
|
+
autoload :VectorField, "bit-struct/vector-field"
|
270
|
+
end
|
271
|
+
|
272
|
+
autoload :Vector, "bit-struct/vector"
|
273
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
class BitStruct
|
2
|
+
# Class for floats (single and double precision) in network order.
|
3
|
+
# Declared with BitStruct.float.
|
4
|
+
class FloatField < Field
|
5
|
+
# Used in describe.
|
6
|
+
def self.class_name
|
7
|
+
@class_name ||= "float"
|
8
|
+
end
|
9
|
+
|
10
|
+
def add_accessors_to(cl, attr = name) # :nodoc:
|
11
|
+
unless offset % 8 == 0
|
12
|
+
raise ArgumentError,
|
13
|
+
"Bad offset, #{offset}, for #{self.class} #{name}." +
|
14
|
+
" Must be multiple of 8."
|
15
|
+
end
|
16
|
+
|
17
|
+
unless length == 32 or length == 64
|
18
|
+
raise ArgumentError,
|
19
|
+
"Bad length, #{length}, for #{self.class} #{name}." +
|
20
|
+
" Must be 32 or 64."
|
21
|
+
end
|
22
|
+
|
23
|
+
offset_byte = offset / 8
|
24
|
+
length_byte = length / 8
|
25
|
+
last_byte = offset_byte + length_byte - 1
|
26
|
+
byte_range = offset_byte..last_byte
|
27
|
+
|
28
|
+
endian = (options[:endian] || options["endian"]).to_s
|
29
|
+
case endian
|
30
|
+
when "native"
|
31
|
+
ctl = case length
|
32
|
+
when 32; "f"
|
33
|
+
when 64; "d"
|
34
|
+
end
|
35
|
+
when "little"
|
36
|
+
ctl = case length
|
37
|
+
when 32; "e"
|
38
|
+
when 64; "E"
|
39
|
+
end
|
40
|
+
when "network", "big", ""
|
41
|
+
ctl = case length
|
42
|
+
when 32; "g"
|
43
|
+
when 64; "G"
|
44
|
+
end
|
45
|
+
else
|
46
|
+
raise ArgumentError,
|
47
|
+
"Unrecognized endian option: #{endian.inspect}"
|
48
|
+
end
|
49
|
+
|
50
|
+
cl.class_eval do
|
51
|
+
define_method attr do ||
|
52
|
+
self[byte_range].unpack(ctl).first
|
53
|
+
end
|
54
|
+
|
55
|
+
define_method "#{attr}=" do |val|
|
56
|
+
self[byte_range] = [val].pack(ctl)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'bit-struct/char-field'
|
2
|
+
|
3
|
+
class BitStruct
|
4
|
+
# Class for char fields that can be accessed with values like
|
5
|
+
# "xx:xx:xx:xx", where each xx is up to 2 hex digits representing a
|
6
|
+
# single octet. The original string-based accessors are still available with
|
7
|
+
# the <tt>_chars</tt> suffix.
|
8
|
+
#
|
9
|
+
# Declared with BitStruct.hex_octets.
|
10
|
+
class HexOctetField < BitStruct::OctetField
|
11
|
+
# Used in describe.
|
12
|
+
def self.class_name
|
13
|
+
@class_name ||= "hex_octets"
|
14
|
+
end
|
15
|
+
|
16
|
+
SEPARATOR = ":"
|
17
|
+
FORMAT = "%02x"
|
18
|
+
BASE = 16
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'bit-struct/bit-struct'
|
2
|
+
|
3
|
+
class BitStruct
|
4
|
+
# Class for nesting a BitStruct as a field within another BitStruct.
|
5
|
+
# Declared with BitStruct.nest.
|
6
|
+
class NestedField < Field
|
7
|
+
def initialize(*args)
|
8
|
+
super
|
9
|
+
end
|
10
|
+
|
11
|
+
# Used in describe.
|
12
|
+
def self.class_name
|
13
|
+
@class_name ||= "nest"
|
14
|
+
end
|
15
|
+
|
16
|
+
def class_name
|
17
|
+
@class_name ||= nested_class.name[/\w+$/]
|
18
|
+
end
|
19
|
+
|
20
|
+
def nested_class
|
21
|
+
@nested_class ||= options[:nested_class] || options["nested_class"]
|
22
|
+
end
|
23
|
+
|
24
|
+
def describe opts
|
25
|
+
if opts[:expand]
|
26
|
+
opts = opts.dup
|
27
|
+
opts[:byte_offset] = offset / 8
|
28
|
+
opts[:omit_header] = opts[:omit_footer] = true
|
29
|
+
nested_class.describe(nil, opts) {|desc| yield desc}
|
30
|
+
else
|
31
|
+
super
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def add_accessors_to(cl, attr = name) # :nodoc:
|
36
|
+
unless offset % 8 == 0
|
37
|
+
raise ArgumentError,
|
38
|
+
"Bad offset, #{offset}, for nested field #{name}." +
|
39
|
+
" Must be multiple of 8."
|
40
|
+
end
|
41
|
+
|
42
|
+
unless length % 8 == 0
|
43
|
+
raise ArgumentError,
|
44
|
+
"Bad length, #{length}, for nested field #{name}." +
|
45
|
+
" Must be multiple of 8."
|
46
|
+
end
|
47
|
+
|
48
|
+
offset_byte = offset / 8
|
49
|
+
length_byte = length / 8
|
50
|
+
last_byte = offset_byte + length_byte - 1
|
51
|
+
byte_range = offset_byte..last_byte
|
52
|
+
|
53
|
+
nc = nested_class
|
54
|
+
|
55
|
+
cl.class_eval do
|
56
|
+
define_method attr do ||
|
57
|
+
nc.new(self[byte_range])
|
58
|
+
end
|
59
|
+
|
60
|
+
define_method "#{attr}=" do |val|
|
61
|
+
if val.length != length_byte
|
62
|
+
raise ArgumentError, "Size mismatch in nested struct assignment " +
|
63
|
+
"to #{attr} with value #{val.inspect}"
|
64
|
+
end
|
65
|
+
|
66
|
+
if val.class != nc
|
67
|
+
warn "Type mismatch in nested struct assignment " +
|
68
|
+
"to #{attr} with value #{val.inspect}"
|
69
|
+
end
|
70
|
+
|
71
|
+
self[byte_range] = val
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|