ruby-protocol-buffers 0.8.4
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/.document +3 -0
- data/.gitignore +11 -0
- data/LICENSE +22 -0
- data/README +82 -0
- data/Rakefile +56 -0
- data/VERSION +1 -0
- data/bin/ruby-protoc +55 -0
- data/debian/changelog +105 -0
- data/debian/compatability +1 -0
- data/debian/control +12 -0
- data/debian/libprotobuf-ruby1.8.install +7 -0
- data/debian/protocol_buffers.rb +3 -0
- data/debian/rules +13 -0
- data/examples/json_protobuf.rb +90 -0
- data/ext/extconf.disabled.rb +3 -0
- data/ext/varint.c +65 -0
- data/lib/protocol_buffers.rb +5 -0
- data/lib/protocol_buffers/compiler.rb +48 -0
- data/lib/protocol_buffers/compiler/descriptor.pb.rb +236 -0
- data/lib/protocol_buffers/compiler/descriptor.proto +393 -0
- data/lib/protocol_buffers/compiler/file_descriptor_to_d.rb +195 -0
- data/lib/protocol_buffers/compiler/file_descriptor_to_ruby.rb +173 -0
- data/lib/protocol_buffers/limited_io.rb +33 -0
- data/lib/protocol_buffers/runtime/decoder.rb +65 -0
- data/lib/protocol_buffers/runtime/encoder.rb +53 -0
- data/lib/protocol_buffers/runtime/enum.rb +4 -0
- data/lib/protocol_buffers/runtime/extend.rb +1 -0
- data/lib/protocol_buffers/runtime/field.rb +550 -0
- data/lib/protocol_buffers/runtime/message.rb +494 -0
- data/lib/protocol_buffers/runtime/service.rb +1 -0
- data/lib/protocol_buffers/runtime/varint.rb +62 -0
- data/spec/compiler_spec.rb +19 -0
- data/spec/fields_spec.rb +49 -0
- data/spec/proto_files/depends.proto +5 -0
- data/spec/proto_files/featureful.proto +54 -0
- data/spec/proto_files/no_package.proto +10 -0
- data/spec/proto_files/simple.proto +5 -0
- data/spec/runtime_spec.rb +430 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +8 -0
- metadata +128 -0
@@ -0,0 +1,53 @@
|
|
1
|
+
module ProtocolBuffers
|
2
|
+
|
3
|
+
class EncodeError < StandardError; end
|
4
|
+
|
5
|
+
module Encoder # :nodoc: all
|
6
|
+
def self.encode(io, message)
|
7
|
+
unless message.valid?
|
8
|
+
raise(EncodeError, "invalid message")
|
9
|
+
end
|
10
|
+
|
11
|
+
message.fields.each do |tag, field|
|
12
|
+
next unless message.value_for_tag?(tag)
|
13
|
+
|
14
|
+
value = message.value_for_tag(tag)
|
15
|
+
wire_type = field.wire_type
|
16
|
+
tag = (field.tag << 3) | wire_type
|
17
|
+
|
18
|
+
if field.repeated?
|
19
|
+
value.each { |i| serialize_field(io, tag, wire_type,
|
20
|
+
field.serialize(i)) }
|
21
|
+
else
|
22
|
+
serialize_field(io, tag, wire_type, field.serialize(value))
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
message.each_unknown_field do |tag_int, value|
|
27
|
+
wire_type = tag_int & 0b111
|
28
|
+
serialize_field(io, tag_int, wire_type, value)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.serialize_field(io, tag, wire_type, serialized)
|
33
|
+
# write the tag
|
34
|
+
Varint.encode(io, tag)
|
35
|
+
|
36
|
+
# see comment in decoder.rb about magic numbers
|
37
|
+
case wire_type
|
38
|
+
when 0 # VARINT
|
39
|
+
Varint.encode(io, serialized)
|
40
|
+
when 1, 5 # FIXED64, FIXED32
|
41
|
+
io.write(serialized)
|
42
|
+
when 2 # LENGTH_DELIMITED
|
43
|
+
Varint.encode(io, serialized.length)
|
44
|
+
io.write(serialized)
|
45
|
+
when 3, 4 # deprecated START_GROUP/END_GROUP types
|
46
|
+
raise(EncodeError, "groups are deprecated and unsupported")
|
47
|
+
else
|
48
|
+
raise(EncodeError, "unknown wire type: #{wire_type}")
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
# TODO
|
@@ -0,0 +1,550 @@
|
|
1
|
+
require 'protocol_buffers'
|
2
|
+
require 'protocol_buffers/runtime/enum'
|
3
|
+
require 'protocol_buffers/runtime/varint'
|
4
|
+
require 'protocol_buffers/limited_io'
|
5
|
+
|
6
|
+
module ProtocolBuffers
|
7
|
+
|
8
|
+
module WireTypes # :nodoc:
|
9
|
+
VARINT = 0
|
10
|
+
FIXED64 = 1
|
11
|
+
LENGTH_DELIMITED = 2
|
12
|
+
START_GROUP = 3 # deprecated, not supported in ruby
|
13
|
+
END_GROUP = 4 # ditto
|
14
|
+
FIXED32 = 5
|
15
|
+
end
|
16
|
+
|
17
|
+
# Acts like an Array, but type-checks each element
|
18
|
+
class RepeatedField < Array # :nodoc:
|
19
|
+
def initialize(field)
|
20
|
+
super()
|
21
|
+
@field = field
|
22
|
+
end
|
23
|
+
|
24
|
+
# ovverride all mutating methods.
|
25
|
+
# I'm sure this will break down on each new major Ruby release, as new
|
26
|
+
# mutating methods are added to Array. Ah, well. caveat emptor.
|
27
|
+
|
28
|
+
def <<(obj)
|
29
|
+
check(obj)
|
30
|
+
super
|
31
|
+
end
|
32
|
+
|
33
|
+
def []=(*args)
|
34
|
+
obj = args.last
|
35
|
+
case obj
|
36
|
+
when nil
|
37
|
+
check(obj) if args.length == 2 && !args.first.is_a?(Range)
|
38
|
+
when Array
|
39
|
+
check_each(obj)
|
40
|
+
else
|
41
|
+
check(obj)
|
42
|
+
end
|
43
|
+
super
|
44
|
+
end
|
45
|
+
|
46
|
+
def collect!(&b)
|
47
|
+
replace(collect(&b))
|
48
|
+
end
|
49
|
+
alias_method :map!, :collect!
|
50
|
+
|
51
|
+
def concat(rhs)
|
52
|
+
check_each(rhs)
|
53
|
+
super
|
54
|
+
end
|
55
|
+
|
56
|
+
def fill(*args, &b)
|
57
|
+
if block_given?
|
58
|
+
super(*args) { |v| check(b.call(v)) }
|
59
|
+
else
|
60
|
+
check(args.first)
|
61
|
+
super
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def insert(index, *objs)
|
66
|
+
check_each(objs)
|
67
|
+
super
|
68
|
+
end
|
69
|
+
|
70
|
+
def push(*objs)
|
71
|
+
check_each(objs)
|
72
|
+
super
|
73
|
+
end
|
74
|
+
|
75
|
+
def replace(array)
|
76
|
+
check_each(array)
|
77
|
+
super
|
78
|
+
end
|
79
|
+
|
80
|
+
def unshift(*objs)
|
81
|
+
check_each(objs)
|
82
|
+
super
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def check(value)
|
88
|
+
@field.check_valid(value)
|
89
|
+
end
|
90
|
+
|
91
|
+
def check_each(iter)
|
92
|
+
iter.each { |value| @field.check_valid(value) }
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
class Field # :nodoc: all
|
97
|
+
attr_reader :otype, :name, :tag
|
98
|
+
|
99
|
+
def repeated?; otype == :repeated end
|
100
|
+
|
101
|
+
def self.create(sender, otype, type, name, tag, opts = {})
|
102
|
+
if type.is_a?(Symbol)
|
103
|
+
klass = Field.const_get("#{type.to_s.capitalize}Field") rescue nil
|
104
|
+
raise("Type not found: #{type}") if klass.nil?
|
105
|
+
field = klass.new(otype, name, tag, opts)
|
106
|
+
elsif type.ancestors.include?(ProtocolBuffers::Enum)
|
107
|
+
field = Field::EnumField.new(type, otype, name, tag, opts)
|
108
|
+
elsif type.ancestors.include?(ProtocolBuffers::Message)
|
109
|
+
field = Field::MessageField.new(type, otype, name, tag, opts)
|
110
|
+
else
|
111
|
+
raise("Type not found: #{type}")
|
112
|
+
end
|
113
|
+
return field
|
114
|
+
end
|
115
|
+
|
116
|
+
def initialize(otype, name, tag, opts = {})
|
117
|
+
@otype = otype
|
118
|
+
@name = name
|
119
|
+
@tag = tag
|
120
|
+
@opts = opts.dup
|
121
|
+
end
|
122
|
+
|
123
|
+
def add_methods_to(klass)
|
124
|
+
klass.class_eval <<-EOF, __FILE__, __LINE__+1
|
125
|
+
attr_reader :#{name}
|
126
|
+
EOF
|
127
|
+
if repeated?
|
128
|
+
klass.class_eval <<-EOF, __FILE__, __LINE__+1
|
129
|
+
def #{name}=(value)
|
130
|
+
if value.nil?
|
131
|
+
@#{name}.clear
|
132
|
+
else
|
133
|
+
@#{name}.clear
|
134
|
+
value.each { |i| @#{name}.push i }
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def has_#{name}?; true; end
|
139
|
+
EOF
|
140
|
+
else
|
141
|
+
klass.class_eval <<-EOF, __FILE__, __LINE__+1
|
142
|
+
def #{name}=(value)
|
143
|
+
field = fields[#{tag}]
|
144
|
+
if value.nil?
|
145
|
+
@set_fields.delete_at(#{tag})
|
146
|
+
@#{name} = field.default_value
|
147
|
+
else
|
148
|
+
field.check_valid(value)
|
149
|
+
@set_fields[#{tag}] = true
|
150
|
+
@#{name} = value
|
151
|
+
if @parent_for_notify
|
152
|
+
@parent_for_notify.default_changed(@tag_for_notify)
|
153
|
+
@parent_for_notify = @tag_for_notify = nil
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def has_#{name}?
|
159
|
+
value_for_tag?(#{tag})
|
160
|
+
end
|
161
|
+
EOF
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def check_value(value)
|
166
|
+
# pass
|
167
|
+
end
|
168
|
+
|
169
|
+
def valid_type?(value)
|
170
|
+
true
|
171
|
+
end
|
172
|
+
|
173
|
+
def inspect_value(value)
|
174
|
+
value.inspect
|
175
|
+
end
|
176
|
+
|
177
|
+
def check_valid(value)
|
178
|
+
raise(TypeError, "can't assign #{value.class.name} to #{self.class.name}") unless valid_type?(value)
|
179
|
+
check_value(value)
|
180
|
+
end
|
181
|
+
|
182
|
+
# the type of value to return depends on the wire_type of the field:
|
183
|
+
# VARINT => Integer
|
184
|
+
# FIXED64 => 8-byte string
|
185
|
+
# LENGTH_DELIMITED => string
|
186
|
+
# FIXED32 => 4-byte string
|
187
|
+
def serialize(value)
|
188
|
+
value
|
189
|
+
end
|
190
|
+
|
191
|
+
# the type of value passed in depends on the wire_type of the field:
|
192
|
+
# VARINT => Integer (Fixnum or Bignum)
|
193
|
+
# FIXED64 => 8-byte string
|
194
|
+
# LENGTH_DELIMITED => IO class, make sure to consume all data available
|
195
|
+
# FIXED32 => 4-byte string
|
196
|
+
def deserialize(value)
|
197
|
+
value
|
198
|
+
end
|
199
|
+
|
200
|
+
module WireFormats
|
201
|
+
module LENGTH_DELIMITED
|
202
|
+
def wire_type
|
203
|
+
WireTypes::LENGTH_DELIMITED
|
204
|
+
end
|
205
|
+
|
206
|
+
def deserialize(value)
|
207
|
+
value.read
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
module VARINT
|
212
|
+
def wire_type
|
213
|
+
WireTypes::VARINT
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
module FIXED32
|
218
|
+
def wire_type
|
219
|
+
WireTypes::FIXED32
|
220
|
+
end
|
221
|
+
|
222
|
+
def serialize(value)
|
223
|
+
[value].pack(pack_code)
|
224
|
+
end
|
225
|
+
|
226
|
+
def deserialize(value)
|
227
|
+
value.unpack(pack_code).first
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
module FIXED64
|
232
|
+
def wire_type
|
233
|
+
WireTypes::FIXED64
|
234
|
+
end
|
235
|
+
|
236
|
+
def serialize(value)
|
237
|
+
[value].pack(pack_code)
|
238
|
+
end
|
239
|
+
|
240
|
+
def deserialize(value)
|
241
|
+
value.unpack(pack_code).first
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
class BytesField < Field
|
247
|
+
include WireFormats::LENGTH_DELIMITED
|
248
|
+
|
249
|
+
def valid_type?(value)
|
250
|
+
value.is_a?(String)
|
251
|
+
end
|
252
|
+
|
253
|
+
def default_value
|
254
|
+
@default_value || @default_value = (@opts[:default] || "").freeze
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
class StringField < BytesField
|
259
|
+
# TODO: UTF-8 validation
|
260
|
+
# Make sure to handle this weird case: strings are mutable, so a UTF-8
|
261
|
+
# valid string could be assigned to a repeated field and then modified in
|
262
|
+
# place later on to not be valid UTF-8 anymore.
|
263
|
+
#
|
264
|
+
# Maybe we just punt on this except in Ruby 1.9 where we can rely on the
|
265
|
+
# language ensuring the string is always UTF-8?
|
266
|
+
end
|
267
|
+
|
268
|
+
class NumericField < Field
|
269
|
+
def min
|
270
|
+
0
|
271
|
+
end
|
272
|
+
|
273
|
+
def max
|
274
|
+
1.0 / 0.0
|
275
|
+
end
|
276
|
+
|
277
|
+
def check_value(value)
|
278
|
+
raise(ArgumentError, "value is out of range for type #{self.class.name}: #{value}") unless value >= min && value <= max
|
279
|
+
end
|
280
|
+
|
281
|
+
def default_value
|
282
|
+
@opts[:default] || 0
|
283
|
+
end
|
284
|
+
|
285
|
+
private
|
286
|
+
# base class, not used directly
|
287
|
+
def initialize(*a); super; end
|
288
|
+
end
|
289
|
+
|
290
|
+
class VarintField < NumericField
|
291
|
+
include WireFormats::VARINT
|
292
|
+
|
293
|
+
def valid_type?(value)
|
294
|
+
value.is_a?(Integer)
|
295
|
+
end
|
296
|
+
|
297
|
+
private
|
298
|
+
# base class, not used directly
|
299
|
+
def initialize(*a); super; end
|
300
|
+
end
|
301
|
+
|
302
|
+
class SignedVarintField < VarintField
|
303
|
+
def deserialize(value)
|
304
|
+
if value > max
|
305
|
+
value - (1<<bits)
|
306
|
+
else
|
307
|
+
value
|
308
|
+
end
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
class Uint32Field < VarintField
|
313
|
+
def max
|
314
|
+
0xFFFFFFFF
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
class Uint64Field < VarintField
|
319
|
+
def max
|
320
|
+
0xFFFFFFFF_FFFFFFFF
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
class Fixed32Field < NumericField
|
325
|
+
include WireFormats::FIXED32
|
326
|
+
|
327
|
+
def pack_code
|
328
|
+
'L'
|
329
|
+
end
|
330
|
+
|
331
|
+
def max
|
332
|
+
0xFFFFFFFF
|
333
|
+
end
|
334
|
+
|
335
|
+
def valid_type?(value)
|
336
|
+
value.is_a?(Integer)
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
340
|
+
class Fixed64Field < NumericField
|
341
|
+
include WireFormats::FIXED64
|
342
|
+
|
343
|
+
def pack_code
|
344
|
+
'Q'
|
345
|
+
end
|
346
|
+
|
347
|
+
def max
|
348
|
+
0xFFFFFFFF_FFFFFFFF
|
349
|
+
end
|
350
|
+
|
351
|
+
def valid_type?(value)
|
352
|
+
value.is_a?(Integer)
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
class Int32Field < SignedVarintField
|
357
|
+
def min
|
358
|
+
-(1 << 31)
|
359
|
+
end
|
360
|
+
|
361
|
+
def max
|
362
|
+
(1 << 31) - 1
|
363
|
+
end
|
364
|
+
|
365
|
+
def bits
|
366
|
+
32
|
367
|
+
end
|
368
|
+
end
|
369
|
+
|
370
|
+
class Sint32Field < Int32Field
|
371
|
+
def serialize(value)
|
372
|
+
Varint.encodeZigZag32(value)
|
373
|
+
end
|
374
|
+
|
375
|
+
def deserialize(value)
|
376
|
+
Varint.decodeZigZag32(value)
|
377
|
+
end
|
378
|
+
end
|
379
|
+
|
380
|
+
class Sfixed32Field < NumericField
|
381
|
+
include WireFormats::FIXED32
|
382
|
+
|
383
|
+
def pack_code
|
384
|
+
'l'
|
385
|
+
end
|
386
|
+
|
387
|
+
def min
|
388
|
+
-(1 << 31)
|
389
|
+
end
|
390
|
+
|
391
|
+
def max
|
392
|
+
(1 << 31) - 1
|
393
|
+
end
|
394
|
+
|
395
|
+
def valid_type?(value)
|
396
|
+
value.is_a?(Integer)
|
397
|
+
end
|
398
|
+
end
|
399
|
+
|
400
|
+
class Int64Field < SignedVarintField
|
401
|
+
def min
|
402
|
+
-(1 << 63)
|
403
|
+
end
|
404
|
+
|
405
|
+
def max
|
406
|
+
(1 << 63) - 1
|
407
|
+
end
|
408
|
+
|
409
|
+
def bits
|
410
|
+
64
|
411
|
+
end
|
412
|
+
end
|
413
|
+
|
414
|
+
class Sint64Field < Int64Field
|
415
|
+
def serialize(value)
|
416
|
+
Varint.encodeZigZag64(value)
|
417
|
+
end
|
418
|
+
|
419
|
+
def deserialize(value)
|
420
|
+
Varint.decodeZigZag64(value)
|
421
|
+
end
|
422
|
+
end
|
423
|
+
|
424
|
+
class Sfixed64Field < NumericField
|
425
|
+
include WireFormats::FIXED64
|
426
|
+
|
427
|
+
def pack_code
|
428
|
+
'q'
|
429
|
+
end
|
430
|
+
|
431
|
+
def min
|
432
|
+
-(1 << 63)
|
433
|
+
end
|
434
|
+
|
435
|
+
def max
|
436
|
+
(1 << 63) - 1
|
437
|
+
end
|
438
|
+
|
439
|
+
def valid_type?(value)
|
440
|
+
value.is_a?(Integer)
|
441
|
+
end
|
442
|
+
end
|
443
|
+
|
444
|
+
class FloatField < Field
|
445
|
+
include WireFormats::FIXED32
|
446
|
+
|
447
|
+
def pack_code
|
448
|
+
'e'
|
449
|
+
end
|
450
|
+
|
451
|
+
def valid_type?(value)
|
452
|
+
value.is_a?(Numeric)
|
453
|
+
end
|
454
|
+
|
455
|
+
def default_value
|
456
|
+
@opts[:default] || 0.0
|
457
|
+
end
|
458
|
+
end
|
459
|
+
|
460
|
+
class DoubleField < Field
|
461
|
+
include WireFormats::FIXED64
|
462
|
+
|
463
|
+
def pack_code
|
464
|
+
'E'
|
465
|
+
end
|
466
|
+
|
467
|
+
def valid_type?(value)
|
468
|
+
value.is_a?(Numeric)
|
469
|
+
end
|
470
|
+
|
471
|
+
def default_value
|
472
|
+
@opts[:default] || 0.0
|
473
|
+
end
|
474
|
+
end
|
475
|
+
|
476
|
+
class BoolField < VarintField
|
477
|
+
def serialize(value)
|
478
|
+
value ? 1 : 0
|
479
|
+
end
|
480
|
+
|
481
|
+
def valid_type?(value)
|
482
|
+
value == true || value == false
|
483
|
+
end
|
484
|
+
|
485
|
+
def check_value(value); end
|
486
|
+
|
487
|
+
def deserialize(value)
|
488
|
+
value != 0
|
489
|
+
end
|
490
|
+
|
491
|
+
def default_value
|
492
|
+
@opts[:default] || false
|
493
|
+
end
|
494
|
+
end
|
495
|
+
|
496
|
+
class EnumField < Int32Field
|
497
|
+
attr_reader :valid_values, :value_to_name
|
498
|
+
|
499
|
+
def initialize(proxy_enum, otype, name, tag, opts = {})
|
500
|
+
super(otype, name, tag, opts)
|
501
|
+
@proxy_enum = proxy_enum
|
502
|
+
@valid_values = @proxy_enum.constants.map { |c| @proxy_enum.const_get(c) }.sort
|
503
|
+
@value_to_name = @proxy_enum.constants.inject({}) { |h, c|
|
504
|
+
h[@proxy_enum.const_get(c)] = c.to_s; h
|
505
|
+
}
|
506
|
+
end
|
507
|
+
|
508
|
+
def check_value(value)
|
509
|
+
raise(ArgumentError, "value is out of range for #{self.class.name}: #{value}") unless @valid_values.include?(value)
|
510
|
+
end
|
511
|
+
|
512
|
+
def default_value
|
513
|
+
@opts[:default] || @valid_values.first
|
514
|
+
end
|
515
|
+
|
516
|
+
def inspect_value(value)
|
517
|
+
"#{@value_to_name[value]}(#{value})"
|
518
|
+
end
|
519
|
+
end
|
520
|
+
|
521
|
+
class MessageField < Field
|
522
|
+
include WireFormats::LENGTH_DELIMITED
|
523
|
+
|
524
|
+
def initialize(proxy_class, otype, name, tag, opts = {})
|
525
|
+
super(otype, name, tag, opts)
|
526
|
+
@proxy_class = proxy_class
|
527
|
+
end
|
528
|
+
|
529
|
+
def default_value
|
530
|
+
@proxy_class.new
|
531
|
+
end
|
532
|
+
|
533
|
+
def valid_type?(value)
|
534
|
+
value.is_a?(@proxy_class)
|
535
|
+
end
|
536
|
+
|
537
|
+
# TODO: serialize could be more efficient if it used the underlying stream
|
538
|
+
# directly rather than string copying, but that would require knowing the
|
539
|
+
# length beforehand.
|
540
|
+
def serialize(value)
|
541
|
+
value.to_s
|
542
|
+
end
|
543
|
+
|
544
|
+
def deserialize(io)
|
545
|
+
@proxy_class.parse(io)
|
546
|
+
end
|
547
|
+
end
|
548
|
+
|
549
|
+
end
|
550
|
+
end
|