Class: RubySol::Pure::Serializer

Inherits:
Object
  • Object
show all
Includes:
WriteIOHelpers
Defined in:
lib/ruby_sol/pure/serializer.rb

Overview

Pure ruby serializer for AMF0 and AMF3

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from WriteIOHelpers

#byte_order, #byte_order_little?, #pack_double, #pack_int16_network, #pack_int8, #pack_integer, #pack_word32_network

Constructor Details

#initialize(class_mapper) ⇒ Serializer

Pass in the class mapper instance to use when serializing. This enables better caching behavior in the class mapper and allows one to change mappings between serialization attempts.



12
13
14
15
16
17
# File 'lib/ruby_sol/pure/serializer.rb', line 12

def initialize class_mapper
  @class_mapper = class_mapper
  @stream = ""
  @depth = 0
  reset_caches()
end

Instance Attribute Details

#streamObject (readonly)

Returns the value of attribute stream.



7
8
9
# File 'lib/ruby_sol/pure/serializer.rb', line 7

def stream
  @stream
end

#versionObject (readonly)

Returns the value of attribute version.



7
8
9
# File 'lib/ruby_sol/pure/serializer.rb', line 7

def version
  @version
end

Instance Method Details

#amf0_serialize(obj) ⇒ Object



90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/ruby_sol/pure/serializer.rb', line 90

def amf0_serialize obj
  if @ref_cache[obj] != nil
    amf0_write_reference @ref_cache[obj]
  elsif obj.respond_to?(:encode_amf)
    obj.encode_amf(self)
  elsif obj.is_a?(NilClass)
    amf0_write_null
  elsif obj.is_a?(TrueClass) || obj.is_a?(FalseClass)
    amf0_write_boolean obj
  elsif obj.is_a?(Numeric)
    amf0_write_number obj
  elsif obj.is_a?(Symbol) || obj.is_a?(String)
    amf0_write_string obj.to_s
  elsif obj.is_a?(Time)
    amf0_write_time obj
  elsif obj.is_a?(Date)
    amf0_write_date obj
  elsif obj.is_a?(Array)
    amf0_write_array obj
  elsif obj.is_a?(Hash) ||obj.is_a?(Object)
    amf0_write_object obj
  end
end

#amf0_write_array(array) ⇒ Object



170
171
172
173
174
175
176
177
# File 'lib/ruby_sol/pure/serializer.rb', line 170

def amf0_write_array array
  @ref_cache.add_obj array
  @stream << AMF0_STRICT_ARRAY_MARKER
  @stream << pack_word32_network(array.length)
  array.each do |elem|
    amf0_serialize elem
  end
end

#amf0_write_boolean(bool) ⇒ Object



118
119
120
121
# File 'lib/ruby_sol/pure/serializer.rb', line 118

def amf0_write_boolean bool
  @stream << AMF0_BOOLEAN_MARKER
  @stream << pack_int8(bool ? 1 : 0)
end

#amf0_write_date(date) ⇒ Object



159
160
161
162
163
# File 'lib/ruby_sol/pure/serializer.rb', line 159

def amf0_write_date date
  @stream << AMF0_DATE_MARKER
  @stream << pack_double(date.strftime("%Q").to_i)
  @stream << pack_int16_network(0) # Time zone
end

#amf0_write_nullObject



114
115
116
# File 'lib/ruby_sol/pure/serializer.rb', line 114

def amf0_write_null
  @stream << AMF0_NULL_MARKER
end

#amf0_write_number(num) ⇒ Object



123
124
125
126
# File 'lib/ruby_sol/pure/serializer.rb', line 123

def amf0_write_number num
  @stream << AMF0_NUMBER_MARKER
  @stream << pack_double(num)
end

#amf0_write_object(obj, props = nil) ⇒ Object



179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/ruby_sol/pure/serializer.rb', line 179

def amf0_write_object obj, props=nil
  @ref_cache.add_obj obj

  props = @class_mapper.props_for_serialization obj if props.nil?

  # Is it a typed object?
  class_name = @class_mapper.get_as_class_name obj
  if class_name
    class_name = class_name.encode("UTF-8").force_encoding("ASCII-8BIT") if class_name.respond_to?(:encode)
    @stream << AMF0_TYPED_OBJECT_MARKER
    @stream << pack_int16_network(class_name.bytesize)
    @stream << class_name
  else
    @stream << AMF0_OBJECT_MARKER
  end

  # Write prop list
  props.sort.each do |key, value| # Sort keys before writing
    key = key.encode("UTF-8").force_encoding("ASCII-8BIT") if key.respond_to?(:encode)
    @stream << pack_int16_network(key.bytesize)
    @stream << key
    amf0_serialize value
  end

  # Write end
  @stream << pack_int16_network(0)
  @stream << AMF0_OBJECT_END_MARKER
end

#amf0_write_reference(index) ⇒ Object



165
166
167
168
# File 'lib/ruby_sol/pure/serializer.rb', line 165

def amf0_write_reference index
  @stream << AMF0_REFERENCE_MARKER
  @stream << pack_int16_network(index)
end

#amf0_write_string(str) ⇒ Object



128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/ruby_sol/pure/serializer.rb', line 128

def amf0_write_string str
  str = str.encode("UTF-8").force_encoding("ASCII-8BIT") if str.respond_to?(:encode)
  len = str.bytesize
  if len > 2**16-1
    @stream << AMF0_LONG_STRING_MARKER
    @stream << pack_word32_network(len)
  else
    @stream << AMF0_STRING_MARKER
    @stream << pack_int16_network(len)
  end
  @stream << str
end

#amf0_write_string_wo_marker(str) ⇒ Object



141
142
143
144
145
146
147
# File 'lib/ruby_sol/pure/serializer.rb', line 141

def amf0_write_string_wo_marker str
  str = str.encode("UTF-8").force_encoding("ASCII-8BIT") if str.respond_to?(:encode)
  len = str.bytesize
  throw SOLError, 'too long string' if len > 2**16-1
  @stream << pack_int16_network(len)
  @stream << str
end

#amf0_write_time(time) ⇒ Object



149
150
151
152
153
154
155
156
157
# File 'lib/ruby_sol/pure/serializer.rb', line 149

def amf0_write_time time
  @stream << AMF0_DATE_MARKER

  time = time.getutc # Dup and convert to UTC
  milli = (time.to_f * 1000).to_i
  @stream << pack_double(milli)

  @stream << pack_int16_network(0) # Time zone
end

#amf3_serialize(obj) ⇒ Object



208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
# File 'lib/ruby_sol/pure/serializer.rb', line 208

def amf3_serialize obj
  if obj.respond_to?(:encode_amf)
    obj.encode_amf(self)
  elsif obj.is_a?(NilClass)
    amf3_write_null
  elsif obj.is_a?(TrueClass)
    amf3_write_true
  elsif obj.is_a?(FalseClass)
    amf3_write_false
  elsif obj.is_a?(Numeric)
    amf3_write_numeric obj
  elsif obj.is_a?(Symbol) || obj.is_a?(String)
    amf3_write_string obj.to_s
  elsif obj.is_a?(Time)
    amf3_write_time obj
  elsif obj.is_a?(Date)
    amf3_write_date obj
  elsif obj.is_a?(StringIO)
    amf3_write_byte_array obj
  elsif obj.is_a?(Array)
    amf3_write_array obj
  elsif obj.is_a?(Hash) || obj.is_a?(Object)
    amf3_write_object obj
  end
end

#amf3_write_array(array) ⇒ Object



308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
# File 'lib/ruby_sol/pure/serializer.rb', line 308

def amf3_write_array array
  # Is it an array collection?
  is_ac = false
  if array.respond_to?(:is_array_collection?)
    is_ac = array.is_array_collection?
  else
    is_ac = @class_mapper.use_array_collection
  end

  # Write type marker
  @stream << (is_ac ? AMF3_OBJECT_MARKER : AMF3_ARRAY_MARKER)

  # Write reference or cache array
  if @object_cache[array] != nil
    amf3_write_reference @object_cache[array]
    return
  else
    @object_cache.add_obj array
    @object_cache.add_obj nil if is_ac # The array collection source array
  end

  # Write out traits and array marker if it's an array collection
  if is_ac
    class_name = "flex.messaging.io.ArrayCollection"
    if @trait_cache[class_name] != nil
      @stream << pack_integer(@trait_cache[class_name] << 2 | 0x01)
    else
      @trait_cache.add_obj class_name
      @stream << "\a" # Externalizable, non-dynamic
      amf3_write_utf8_vr(class_name)
    end
    @stream << AMF3_ARRAY_MARKER
  end

  # Build AMF string for array
  header = array.length << 1 # make room for a low bit of 1
  header = header | 1 # set the low bit to 1
  @stream << pack_integer(header)
  @stream << AMF3_CLOSE_DYNAMIC_ARRAY
  array.each do |elem|
    amf3_serialize elem
  end
end

#amf3_write_byte_array(array) ⇒ Object



296
297
298
299
300
301
302
303
304
305
306
# File 'lib/ruby_sol/pure/serializer.rb', line 296

def amf3_write_byte_array array
  @stream << AMF3_BYTE_ARRAY_MARKER
  if @object_cache[array] != nil
    amf3_write_reference @object_cache[array]
  else
    @object_cache.add_obj array
    str = array.string
    @stream << pack_integer(str.bytesize << 1 | 1)
    @stream << str
  end
end

#amf3_write_date(date) ⇒ Object



282
283
284
285
286
287
288
289
290
291
292
293
294
# File 'lib/ruby_sol/pure/serializer.rb', line 282

def amf3_write_date date
  @stream << AMF3_DATE_MARKER
  if @object_cache[date] != nil
    amf3_write_reference @object_cache[date]
  else
    # Cache date
    @object_cache.add_obj date

    # Build AMF string
    @stream << AMF3_NULL_MARKER
    @stream << pack_double(date.strftime("%Q").to_i)
  end
end

#amf3_write_falseObject



247
248
249
# File 'lib/ruby_sol/pure/serializer.rb', line 247

def amf3_write_false
  @stream << AMF3_FALSE_MARKER
end

#amf3_write_nullObject



239
240
241
# File 'lib/ruby_sol/pure/serializer.rb', line 239

def amf3_write_null
  @stream << AMF3_NULL_MARKER
end

#amf3_write_numeric(num) ⇒ Object



251
252
253
254
255
256
257
258
259
# File 'lib/ruby_sol/pure/serializer.rb', line 251

def amf3_write_numeric num
  if !num.integer? || num < MIN_INTEGER || num > MAX_INTEGER # Check valid range for 29 bits
    @stream << AMF3_DOUBLE_MARKER
    @stream << pack_double(num)
  else
    @stream << AMF3_INTEGER_MARKER
    @stream << pack_integer(num)
  end
end

#amf3_write_object(obj, props = nil, traits = nil) ⇒ Object



352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
# File 'lib/ruby_sol/pure/serializer.rb', line 352

def amf3_write_object obj, props=nil, traits=nil
  @stream << AMF3_OBJECT_MARKER

  # Caching...
  if @object_cache[obj] != nil
    amf3_write_reference @object_cache[obj]
    return
  end
  @object_cache.add_obj obj

  # Calculate traits if not given
  is_default = false
  if traits.nil?
    traits = {
              :class_name => @class_mapper.get_as_class_name(obj),
              :members => [],
              :externalizable => false,
              :dynamic => true
             }
    is_default = true unless traits[:class_name]
  end
  class_name = is_default ? "__default__" : traits[:class_name]

  # Write out traits
  if (class_name && @trait_cache[class_name] != nil)
    @stream << pack_integer(@trait_cache[class_name] << 2 | 0x01)
  else
    @trait_cache.add_obj class_name if class_name

    # Write out trait header
    header = 0x03 # Not object ref and not trait ref
    header |= 0x02 << 2 if traits[:dynamic]
    header |= 0x01 << 2 if traits[:externalizable]
    header |= traits[:members].length << 4
    @stream << pack_integer(header)

    # Write out class name
    if class_name == "__default__"
      amf3_write_utf8_vr("")
    else
      amf3_write_utf8_vr(class_name.to_s)
    end

    # Write out members
    traits[:members].each {|m| amf3_write_utf8_vr(m)}
  end

  # If externalizable, take externalized data shortcut
  if traits[:externalizable]
    obj.write_external(self)
    return
  end

  # Extract properties if not given
  props = @class_mapper.props_for_serialization(obj) if props.nil?

  # Write out sealed properties
  traits[:members].each do |m|
    amf3_serialize props[m]
    props.delete(m)
  end

  # Write out dynamic properties
  if traits[:dynamic]
    # Write out dynamic properties
    props.sort.each do |key, val| # Sort props until Ruby 1.9 becomes common
      amf3_write_utf8_vr key.to_s
      amf3_serialize val
    end

    # Write close
    @stream << AMF3_CLOSE_DYNAMIC_OBJECT
  end
end

#amf3_write_reference(index) ⇒ Object



234
235
236
237
# File 'lib/ruby_sol/pure/serializer.rb', line 234

def amf3_write_reference index
  header = index << 1 # shift value left to leave a low bit of 0
  @stream << pack_integer(header)
end

#amf3_write_string(str) ⇒ Object



261
262
263
264
# File 'lib/ruby_sol/pure/serializer.rb', line 261

def amf3_write_string str
  @stream << AMF3_STRING_MARKER
  amf3_write_utf8_vr str
end

#amf3_write_time(time) ⇒ Object



266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
# File 'lib/ruby_sol/pure/serializer.rb', line 266

def amf3_write_time time
  @stream << AMF3_DATE_MARKER
  if @object_cache[time] != nil
    amf3_write_reference @object_cache[time]
  else
    # Cache time
    @object_cache.add_obj time

    # Build AMF string
    time = time.getutc # Dup and convert to UTC
    milli = (time.to_f * 1000).to_i
    @stream << AMF3_NULL_MARKER
    @stream << pack_double(milli)
  end
end

#amf3_write_trueObject



243
244
245
# File 'lib/ruby_sol/pure/serializer.rb', line 243

def amf3_write_true
  @stream << AMF3_TRUE_MARKER
end

#amf3_write_utf8_vr(str, encode = true) ⇒ Object



427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
# File 'lib/ruby_sol/pure/serializer.rb', line 427

def amf3_write_utf8_vr str, encode=true
  if str.respond_to?(:encode)
    if encode
      str = str.encode("UTF-8")
    else
      str = str.dup if str.frozen?
    end
    str.force_encoding("ASCII-8BIT")
  end

  if str == ''
    @stream << AMF3_EMPTY_STRING
  elsif @string_cache[str] != nil
    amf3_write_reference @string_cache[str]
  else
    # Cache string
    @string_cache.add_obj str

    # Build AMF string
    @stream << pack_integer(str.bytesize << 1 | 1)
    @stream << str
  end
end

#reset_cachesObject



19
20
21
22
23
24
# File 'lib/ruby_sol/pure/serializer.rb', line 19

def reset_caches
  @ref_cache = SerializerCache.new :object
  @string_cache = SerializerCache.new :string
  @object_cache = SerializerCache.new :object
  @trait_cache = SerializerCache.new :string
end

#serialize(version, obj) ⇒ Object

Serialize the given object using AMF0 or AMF3. Can be called from inside encode_amf, but make sure to pass in the proper version or it may not be possible to decode. Use the serializer version attribute for this.

Raises:

  • (ArgumentError)


29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/ruby_sol/pure/serializer.rb', line 29

def serialize version, obj
  raise ArgumentError, "unsupported version #{version}" unless [0,3].include?(version)
  @version = version

  # Initialize caches
  if @depth == 0
    if @version == 0
      @ref_cache = SerializerCache.new :object
    else
      @string_cache = SerializerCache.new :string
      @object_cache = SerializerCache.new :object
      @trait_cache = SerializerCache.new :string
    end
  end
  @depth += 1

  # Perform serialization
  if @version == 0
    amf0_serialize(obj)
  else
    amf3_serialize(obj)
  end

  # Cleanup
  @depth -= 1
  if @depth == 0
    @ref_cache = nil
    @string_cache = nil
    @object_cache = nil
    @trait_cache = nil
  end

  return @stream
end

#write_array(arr) ⇒ Object

Helper for writing arrays inside encode_amf. It uses the current AMF version to write the array.



66
67
68
69
70
71
72
# File 'lib/ruby_sol/pure/serializer.rb', line 66

def write_array arr
  if @version == 0
    amf0_write_array arr
  else
    amf3_write_array arr
  end
end

#write_object(obj, props = nil, traits = nil) ⇒ Object

Helper for writing objects inside encode_amf. It uses the current AMF version to write the object. If you pass in a property hash, it will use it rather than having the class mapper determine properties. For AMF3, you can also specify a traits hash, which can be used to reduce serialized data size or serialize things as externalizable.



79
80
81
82
83
84
85
# File 'lib/ruby_sol/pure/serializer.rb', line 79

def write_object obj, props=nil, traits=nil
  if @version == 0
    amf0_write_object obj, props
  else
    amf3_write_object obj, props, traits
  end
end