scotttam-RocketAMF 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (81) hide show
  1. data/README.rdoc +45 -0
  2. data/Rakefile +54 -0
  3. data/lib/rocketamf.rb +128 -0
  4. data/lib/rocketamf/class_mapping.rb +231 -0
  5. data/lib/rocketamf/constants.rb +46 -0
  6. data/lib/rocketamf/pure.rb +28 -0
  7. data/lib/rocketamf/pure/deserializer.rb +419 -0
  8. data/lib/rocketamf/pure/io_helpers.rb +94 -0
  9. data/lib/rocketamf/pure/remoting.rb +134 -0
  10. data/lib/rocketamf/pure/serializer.rb +433 -0
  11. data/lib/rocketamf/remoting.rb +144 -0
  12. data/lib/rocketamf/values/array_collection.rb +13 -0
  13. data/lib/rocketamf/values/messages.rb +133 -0
  14. data/lib/rocketamf/values/typed_hash.rb +13 -0
  15. data/spec/amf/class_mapping_spec.rb +150 -0
  16. data/spec/amf/deserializer_spec.rb +367 -0
  17. data/spec/amf/remoting_spec.rb +132 -0
  18. data/spec/amf/serializer_spec.rb +384 -0
  19. data/spec/amf/values/array_collection_spec.rb +19 -0
  20. data/spec/amf/values/messages_spec.rb +31 -0
  21. data/spec/fixtures/objects/amf0-boolean.bin +1 -0
  22. data/spec/fixtures/objects/amf0-complexEncodedStringArray.bin +0 -0
  23. data/spec/fixtures/objects/amf0-date.bin +0 -0
  24. data/spec/fixtures/objects/amf0-ecma-ordinal-array.bin +0 -0
  25. data/spec/fixtures/objects/amf0-hash.bin +0 -0
  26. data/spec/fixtures/objects/amf0-null.bin +1 -0
  27. data/spec/fixtures/objects/amf0-number.bin +0 -0
  28. data/spec/fixtures/objects/amf0-object.bin +0 -0
  29. data/spec/fixtures/objects/amf0-ref-test.bin +0 -0
  30. data/spec/fixtures/objects/amf0-strict-array.bin +0 -0
  31. data/spec/fixtures/objects/amf0-string.bin +0 -0
  32. data/spec/fixtures/objects/amf0-typed-object.bin +0 -0
  33. data/spec/fixtures/objects/amf0-undefined.bin +1 -0
  34. data/spec/fixtures/objects/amf0-untyped-object.bin +0 -0
  35. data/spec/fixtures/objects/amf0-xmlDoc.bin +0 -0
  36. data/spec/fixtures/objects/amf3-0.bin +0 -0
  37. data/spec/fixtures/objects/amf3-arrayCollection.bin +2 -0
  38. data/spec/fixtures/objects/amf3-arrayRef.bin +1 -0
  39. data/spec/fixtures/objects/amf3-bigNum.bin +0 -0
  40. data/spec/fixtures/objects/amf3-byteArray.bin +0 -0
  41. data/spec/fixtures/objects/amf3-byteArrayRef.bin +1 -0
  42. data/spec/fixtures/objects/amf3-complexEncodedStringArray.bin +1 -0
  43. data/spec/fixtures/objects/amf3-date.bin +0 -0
  44. data/spec/fixtures/objects/amf3-datesRef.bin +0 -0
  45. data/spec/fixtures/objects/amf3-dictionary.bin +0 -0
  46. data/spec/fixtures/objects/amf3-dynObject.bin +2 -0
  47. data/spec/fixtures/objects/amf3-emptyArray.bin +1 -0
  48. data/spec/fixtures/objects/amf3-emptyArrayRef.bin +1 -0
  49. data/spec/fixtures/objects/amf3-emptyDictionary.bin +0 -0
  50. data/spec/fixtures/objects/amf3-emptyStringRef.bin +1 -0
  51. data/spec/fixtures/objects/amf3-encodedStringRef.bin +0 -0
  52. data/spec/fixtures/objects/amf3-false.bin +1 -0
  53. data/spec/fixtures/objects/amf3-float.bin +0 -0
  54. data/spec/fixtures/objects/amf3-graphMember.bin +0 -0
  55. data/spec/fixtures/objects/amf3-hash.bin +2 -0
  56. data/spec/fixtures/objects/amf3-largeMax.bin +0 -0
  57. data/spec/fixtures/objects/amf3-largeMin.bin +0 -0
  58. data/spec/fixtures/objects/amf3-max.bin +1 -0
  59. data/spec/fixtures/objects/amf3-min.bin +0 -0
  60. data/spec/fixtures/objects/amf3-mixedArray.bin +11 -0
  61. data/spec/fixtures/objects/amf3-null.bin +1 -0
  62. data/spec/fixtures/objects/amf3-objRef.bin +0 -0
  63. data/spec/fixtures/objects/amf3-primArray.bin +1 -0
  64. data/spec/fixtures/objects/amf3-string.bin +1 -0
  65. data/spec/fixtures/objects/amf3-stringRef.bin +0 -0
  66. data/spec/fixtures/objects/amf3-symbol.bin +1 -0
  67. data/spec/fixtures/objects/amf3-traitRef.bin +3 -0
  68. data/spec/fixtures/objects/amf3-true.bin +1 -0
  69. data/spec/fixtures/objects/amf3-typedObject.bin +2 -0
  70. data/spec/fixtures/objects/amf3-xml.bin +1 -0
  71. data/spec/fixtures/objects/amf3-xmlDoc.bin +1 -0
  72. data/spec/fixtures/objects/amf3-xmlRef.bin +1 -0
  73. data/spec/fixtures/request/acknowledge-response.bin +0 -0
  74. data/spec/fixtures/request/amf0-error-response.bin +0 -0
  75. data/spec/fixtures/request/commandMessage.bin +0 -0
  76. data/spec/fixtures/request/remotingMessage.bin +0 -0
  77. data/spec/fixtures/request/simple-response.bin +0 -0
  78. data/spec/fixtures/request/unsupportedCommandMessage.bin +0 -0
  79. data/spec/spec.opts +1 -0
  80. data/spec/spec_helper.rb +31 -0
  81. metadata +153 -0
@@ -0,0 +1,94 @@
1
+ module RocketAMF
2
+ module Pure
3
+ module ReadIOHelpers #:nodoc:
4
+ def read_int8 source
5
+ source.read(1).unpack('c').first
6
+ end
7
+
8
+ def read_word8 source
9
+ source.read(1).unpack('C').first
10
+ end
11
+
12
+ def read_double source
13
+ source.read(8).unpack('G').first
14
+ end
15
+
16
+ def read_word16_network source
17
+ source.read(2).unpack('n').first
18
+ end
19
+
20
+ def read_int16_network source
21
+ str = source.read(2)
22
+ str.reverse! if byte_order_little? # swap bytes as native=little (and we want network)
23
+ str.unpack('s').first
24
+ end
25
+
26
+ def read_word32_network source
27
+ source.read(4).unpack('N').first
28
+ end
29
+
30
+ def byte_order
31
+ if [0x12345678].pack("L") == "\x12\x34\x56\x78"
32
+ :BigEndian
33
+ else
34
+ :LittleEndian
35
+ end
36
+ end
37
+
38
+ def byte_order_little?
39
+ (byte_order == :LittleEndian) ? true : false;
40
+ end
41
+ end
42
+
43
+ module WriteIOHelpers #:nodoc:
44
+ def pack_integer(integer)
45
+ integer = integer & 0x1fffffff
46
+ if(integer < 0x80)
47
+ [integer].pack('c')
48
+ elsif(integer < 0x4000)
49
+ [integer >> 7 & 0x7f | 0x80].pack('c')+
50
+ [integer & 0x7f].pack('c')
51
+ elsif(integer < 0x200000)
52
+ [integer >> 14 & 0x7f | 0x80].pack('c') +
53
+ [integer >> 7 & 0x7f | 0x80].pack('c') +
54
+ [integer & 0x7f].pack('c')
55
+ else
56
+ [integer >> 22 & 0x7f | 0x80].pack('c')+
57
+ [integer >> 15 & 0x7f | 0x80].pack('c')+
58
+ [integer >> 8 & 0x7f | 0x80].pack('c')+
59
+ [integer & 0xff].pack('c')
60
+ end
61
+ end
62
+
63
+ def pack_double(double)
64
+ [double].pack('G')
65
+ end
66
+
67
+ def pack_int8(val)
68
+ [val].pack('c')
69
+ end
70
+
71
+ def pack_int16_network(val)
72
+ [val].pack('n')
73
+ end
74
+
75
+ def pack_word32_network(val)
76
+ str = [val].pack('L')
77
+ str.reverse! if byte_order_little? # swap bytes as native=little (and we want network)
78
+ str
79
+ end
80
+
81
+ def byte_order
82
+ if [0x12345678].pack("L") == "\x12\x34\x56\x78"
83
+ :BigEndian
84
+ else
85
+ :LittleEndian
86
+ end
87
+ end
88
+
89
+ def byte_order_little?
90
+ (byte_order == :LittleEndian) ? true : false;
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,134 @@
1
+ require 'rocketamf/pure/io_helpers'
2
+
3
+ module RocketAMF
4
+ module Pure
5
+ # Included into RocketAMF::Envelope, this module replaces the
6
+ # populate_from_stream and serialize methods with actual working versions
7
+ module Envelope
8
+ # Included into RocketAMF::Envelope, this method handles deserializing an
9
+ # AMF request/response into the envelope
10
+ def populate_from_stream stream
11
+ stream = StringIO.new(stream) unless StringIO === stream
12
+
13
+ # Initialize
14
+ @amf_version = 0
15
+ @headers = []
16
+ @messages = []
17
+
18
+ # Read AMF version
19
+ @amf_version = read_word16_network stream
20
+
21
+ # Read in headers
22
+ header_count = read_word16_network stream
23
+ 0.upto(header_count-1) do
24
+ name = stream.read(read_word16_network(stream))
25
+ name.force_encoding("UTF-8") if name.respond_to?(:force_encoding)
26
+ must_understand = read_int8(stream) != 0
27
+ length = read_word32_network stream
28
+ data = RocketAMF.deserialize stream
29
+ @headers << RocketAMF::Header.new(name, must_understand, data)
30
+ end
31
+
32
+ # Read in messages
33
+ message_count = read_word16_network stream
34
+ 0.upto(message_count-1) do
35
+ target_uri = stream.read(read_word16_network(stream))
36
+ target_uri.force_encoding("UTF-8") if target_uri.respond_to?(:force_encoding)
37
+ response_uri = stream.read(read_word16_network(stream))
38
+ response_uri.force_encoding("UTF-8") if response_uri.respond_to?(:force_encoding)
39
+ length = read_word32_network stream
40
+ data = RocketAMF.deserialize stream
41
+ if data.is_a?(Array) && data.length == 1 && data[0].is_a?(::RocketAMF::Values::AbstractMessage)
42
+ data = data[0]
43
+ end
44
+ @messages << RocketAMF::Message.new(target_uri, response_uri, data)
45
+ end
46
+
47
+ self
48
+ end
49
+
50
+ # Included into RocketAMF::Envelope, this method handles serializing an
51
+ # AMF request/response into the envelope
52
+ def serialize
53
+ stream = ""
54
+
55
+ # Write version
56
+ stream << pack_int16_network(@amf_version)
57
+
58
+ # Write headers
59
+ stream << pack_int16_network(@headers.length) # Header count
60
+ @headers.each do |h|
61
+ name_str = h.name
62
+ name_str.encode!("UTF-8").force_encoding("ASCII-8BIT") if name_str.respond_to?(:encode)
63
+ stream << pack_int16_network(name_str.bytesize)
64
+ stream << name_str
65
+ stream << pack_int8(h.must_understand ? 1 : 0)
66
+ stream << pack_word32_network(-1)
67
+ stream << RocketAMF.serialize(h.data, 0)
68
+ end
69
+
70
+ # Write messages
71
+ stream << pack_int16_network(@messages.length) # Message count
72
+ @messages.each do |m|
73
+ uri_str = m.target_uri
74
+ uri_str.encode!("UTF-8").force_encoding("ASCII-8BIT") if uri_str.respond_to?(:encode)
75
+ stream << pack_int16_network(uri_str.bytesize)
76
+ stream << uri_str
77
+
78
+ uri_str = m.response_uri
79
+ uri_str.encode!("UTF-8").force_encoding("ASCII-8BIT") if uri_str.respond_to?(:encode)
80
+ stream << pack_int16_network(uri_str.bytesize)
81
+ stream << uri_str
82
+
83
+ stream << pack_word32_network(-1)
84
+ stream << AMF0_AMF3_MARKER if @amf_version == 3
85
+ stream << RocketAMF.serialize(m.data, @amf_version)
86
+ end
87
+
88
+ stream
89
+ end
90
+
91
+ def deserialize(stream)
92
+ return nil unless stream
93
+ stream = StringIO.new(stream) unless StringIO === stream
94
+
95
+ amf_hash = {}
96
+ amf_hash[:amf_version] = read_int16_network(stream)
97
+
98
+ headers = []
99
+ header_count = read_int16_network(stream)
100
+ header_count.to_i.times do
101
+ header = {}
102
+ header_name_length = read_int16_network(stream)
103
+ header[:name] = stream.read(header_name_length)
104
+ header[:must_understand] = read_int8(stream)
105
+ buffer = read_word32_network(stream)
106
+ header[:content] = RocketAMF.deserialize(stream, @amf_version)
107
+ headers << header
108
+ end
109
+ amf_hash[:headers] = headers
110
+
111
+ bodies = []
112
+ body_count = read_int16_network(stream)
113
+ body_count.to_i.times do
114
+ body = {}
115
+ target_uri_length = read_int16_network(stream)
116
+ body[:target_uri] = stream.read(target_uri_length)
117
+ response_uri_length = read_int16_network(stream)
118
+ body[:response_uri] = stream.read(response_uri_length)
119
+ buffer = read_word32_network(stream)
120
+ amf_version_3 = read_int16_network(stream) if @amf_version == 3
121
+ body[:content] = RocketAMF.deserialize(stream, @amf_version)
122
+ bodies << body
123
+ end
124
+ amf_hash[:bodies] = bodies
125
+
126
+ amf_hash
127
+ end
128
+
129
+ private
130
+ include RocketAMF::Pure::ReadIOHelpers
131
+ include RocketAMF::Pure::WriteIOHelpers
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,433 @@
1
+ require 'rocketamf/pure/io_helpers'
2
+
3
+ module RocketAMF
4
+ module Pure
5
+ # AMF0 implementation of serializer
6
+ class Serializer
7
+ attr_reader :ref_cache, :stream
8
+
9
+ def initialize
10
+ @ref_cache = SerializerCache.new :object
11
+ @stream = ""
12
+ end
13
+
14
+ def version
15
+ 0
16
+ end
17
+
18
+ def serialize obj
19
+ if @ref_cache[obj] != nil
20
+ write_reference @ref_cache[obj]
21
+ elsif obj.respond_to?(:encode_amf)
22
+ obj.encode_amf(self)
23
+ elsif obj.is_a?(NilClass)
24
+ write_null
25
+ elsif obj.is_a?(TrueClass) || obj.is_a?(FalseClass)
26
+ write_boolean obj
27
+ elsif obj.is_a?(Float) || obj.is_a?(Integer)
28
+ write_number obj
29
+ elsif obj.is_a?(Symbol) || obj.is_a?(String)
30
+ write_string obj.to_s
31
+ elsif obj.is_a?(Time)
32
+ write_date obj
33
+ elsif obj.is_a?(Array)
34
+ write_array obj
35
+ elsif obj.is_a?(Hash)
36
+ write_hash obj
37
+ elsif obj.is_a?(Object)
38
+ write_object obj
39
+ end
40
+ @stream
41
+ end
42
+
43
+ def write_null
44
+ @stream << AMF0_NULL_MARKER
45
+ end
46
+
47
+ def write_boolean bool
48
+ @stream << AMF0_BOOLEAN_MARKER
49
+ @stream << pack_int8(bool ? 1 : 0)
50
+ end
51
+
52
+ def write_number num
53
+ @stream << AMF0_NUMBER_MARKER
54
+ @stream << pack_double(num)
55
+ end
56
+
57
+ def write_string str
58
+ str = str.encode("UTF-8").force_encoding("ASCII-8BIT") if str.respond_to?(:encode)
59
+ len = str.bytesize
60
+ if len > 2**16-1
61
+ @stream << AMF0_LONG_STRING_MARKER
62
+ @stream << pack_word32_network(len)
63
+ else
64
+ @stream << AMF0_STRING_MARKER
65
+ @stream << pack_int16_network(len)
66
+ end
67
+ @stream << str
68
+ end
69
+
70
+ def write_date date
71
+ @stream << AMF0_DATE_MARKER
72
+
73
+ date = date.getutc # Dup and convert to UTC
74
+ milli = (date.to_f * 1000).to_i
75
+ @stream << pack_double(milli)
76
+
77
+ @stream << pack_int16_network(0) # Time zone
78
+ end
79
+
80
+ def write_reference index
81
+ @stream << AMF0_REFERENCE_MARKER
82
+ @stream << pack_int16_network(index)
83
+ end
84
+
85
+ def write_array array
86
+ @ref_cache.add_obj array
87
+ @stream << AMF0_STRICT_ARRAY_MARKER
88
+ @stream << pack_word32_network(array.length)
89
+ array.each do |elem|
90
+ serialize elem
91
+ end
92
+ end
93
+
94
+ def write_hash hash
95
+ @ref_cache.add_obj hash
96
+ @stream << AMF0_HASH_MARKER
97
+ @stream << pack_word32_network(hash.length)
98
+ write_prop_list RocketAMF::ClassMapper.props_for_serialization(hash)
99
+ end
100
+
101
+ def write_object obj
102
+ @ref_cache.add_obj obj
103
+ props = RocketAMF::ClassMapper.props_for_serialization obj
104
+ write_custom_object obj, props, false
105
+ end
106
+
107
+ # Used to write out custom objects when writing a custom <tt>encode_amf</tt>
108
+ # method. The class name is taken from the given obj, and the props array
109
+ # is serialized as the contents of the object.
110
+ def write_custom_object obj, props, do_cache=true
111
+ @ref_cache.add_obj obj if do_cache
112
+
113
+ # Is it a typed object?
114
+ class_name = RocketAMF::ClassMapper.get_as_class_name obj
115
+ if class_name
116
+ class_name = class_name.encode("UTF-8").force_encoding("ASCII-8BIT") if class_name.respond_to?(:encode)
117
+ @stream << AMF0_TYPED_OBJECT_MARKER
118
+ @stream << pack_int16_network(class_name.bytesize)
119
+ @stream << class_name
120
+ else
121
+ @stream << AMF0_OBJECT_MARKER
122
+ end
123
+
124
+ write_prop_list props
125
+ end
126
+
127
+ private
128
+ include RocketAMF::Pure::WriteIOHelpers
129
+ def write_prop_list obj
130
+ # Write prop list
131
+ props = RocketAMF::ClassMapper.props_for_serialization obj
132
+ props.sort.each do |key, value| # Sort keys before writing
133
+ key = key.encode("UTF-8").force_encoding("ASCII-8BIT") if key.respond_to?(:encode)
134
+ @stream << pack_int16_network(key.bytesize)
135
+ @stream << key
136
+ serialize value
137
+ end
138
+
139
+ # Write end
140
+ @stream << pack_int16_network(0)
141
+ @stream << AMF0_OBJECT_END_MARKER
142
+ end
143
+ end
144
+
145
+ # AMF3 implementation of serializer
146
+ class AMF3Serializer
147
+ attr_reader :string_cache, :object_cache, :trait_cache, :stream
148
+
149
+ def initialize
150
+ @string_cache = SerializerCache.new :string
151
+ @object_cache = SerializerCache.new :object
152
+ @trait_cache = SerializerCache.new :trait
153
+ @stream = ""
154
+ end
155
+
156
+ def version
157
+ 3
158
+ end
159
+
160
+ def serialize obj
161
+ if obj.respond_to?(:encode_amf)
162
+ obj.encode_amf(self)
163
+ elsif obj.is_a?(NilClass)
164
+ write_null
165
+ elsif obj.is_a?(TrueClass)
166
+ write_true
167
+ elsif obj.is_a?(FalseClass)
168
+ write_false
169
+ elsif obj.is_a?(Float)
170
+ write_float obj
171
+ elsif obj.is_a?(Integer)
172
+ write_integer obj
173
+ elsif obj.is_a?(Symbol) || obj.is_a?(String)
174
+ write_string obj.to_s
175
+ elsif obj.is_a?(Time)
176
+ write_date obj
177
+ elsif obj.is_a?(StringIO)
178
+ write_byte_array obj
179
+ elsif obj.is_a?(RocketAMF::Values::ArrayCollection)
180
+ write_array_collection obj
181
+ elsif obj.is_a?(Array)
182
+ write_array obj
183
+ elsif obj.is_a?(Hash) || obj.is_a?(Object)
184
+ write_object obj
185
+ end
186
+ @stream
187
+ end
188
+
189
+ def write_reference index
190
+ header = index << 1 # shift value left to leave a low bit of 0
191
+ @stream << pack_integer(header)
192
+ end
193
+
194
+ def write_null
195
+ @stream << AMF3_NULL_MARKER
196
+ end
197
+
198
+ def write_true
199
+ @stream << AMF3_TRUE_MARKER
200
+ end
201
+
202
+ def write_false
203
+ @stream << AMF3_FALSE_MARKER
204
+ end
205
+
206
+ def write_integer int
207
+ if int < MIN_INTEGER || int > MAX_INTEGER # Check valid range for 29 bits
208
+ write_float int.to_f
209
+ else
210
+ @stream << AMF3_INTEGER_MARKER
211
+ @stream << pack_integer(int)
212
+ end
213
+ end
214
+
215
+ def write_float float
216
+ @stream << AMF3_DOUBLE_MARKER
217
+ @stream << pack_double(float)
218
+ end
219
+
220
+ def write_string str
221
+ @stream << AMF3_STRING_MARKER
222
+ write_utf8_vr str
223
+ end
224
+
225
+ def write_date date
226
+ @stream << AMF3_DATE_MARKER
227
+ if @object_cache[date] != nil
228
+ write_reference @object_cache[date]
229
+ else
230
+ # Cache date
231
+ @object_cache.add_obj date
232
+
233
+ # Build AMF string
234
+ date = date.getutc # Dup and convert to UTC
235
+ milli = (date.to_f * 1000).to_i
236
+ @stream << AMF3_NULL_MARKER
237
+ @stream << pack_double(milli)
238
+ end
239
+ end
240
+
241
+ def write_byte_array array
242
+ @stream << AMF3_BYTE_ARRAY_MARKER
243
+ if @object_cache[array] != nil
244
+ write_reference @object_cache[array]
245
+ else
246
+ @object_cache.add_obj array
247
+ write_utf8_vr array.string
248
+ end
249
+ end
250
+
251
+ def write_array_collection array
252
+ write_custom_object array, nil, {:class_name => RocketAMF::ClassMapper.get_as_class_name(array), :members => [], :externalizable => true, :dynamic => false}
253
+ end
254
+
255
+ def write_array array
256
+ @stream << AMF3_ARRAY_MARKER
257
+ if @object_cache[array] != nil
258
+ write_reference @object_cache[array]
259
+ else
260
+ # Cache array
261
+ @object_cache.add_obj array
262
+
263
+ # Build AMF string
264
+ header = array.length << 1 # make room for a low bit of 1
265
+ header = header | 1 # set the low bit to 1
266
+ @stream << pack_integer(header)
267
+ @stream << AMF3_CLOSE_DYNAMIC_ARRAY
268
+ array.each do |elem|
269
+ serialize elem
270
+ end
271
+ end
272
+ end
273
+
274
+ def write_object obj, traits=nil
275
+ @stream << AMF3_OBJECT_MARKER
276
+ if @object_cache[obj] != nil
277
+ write_reference @object_cache[obj]
278
+ else
279
+ # Cache object
280
+ @object_cache.add_obj obj
281
+
282
+ # Calculate traits if not given
283
+ if traits.nil?
284
+ traits = {
285
+ :class_name => RocketAMF::ClassMapper.get_as_class_name(obj),
286
+ :members => [],
287
+ :externalizable => false,
288
+ :dynamic => true
289
+ }
290
+ end
291
+
292
+ # Write object
293
+ props = RocketAMF::ClassMapper.props_for_serialization obj
294
+ write_custom_object obj, props, traits, false
295
+ end
296
+ end
297
+
298
+ # Used to write out custom objects when writing a custom <tt>encode_amf</tt>
299
+ # method. The class name is taken from the given obj, and the props array
300
+ # is serialized as the contents of the object.
301
+ def write_custom_object obj, props, traits, do_cache=true
302
+ if do_cache
303
+ @stream << AMF3_OBJECT_MARKER
304
+ if @object_cache[obj] != nil
305
+ write_reference @object_cache[obj]
306
+ return
307
+ else
308
+ # Cache object
309
+ @object_cache.add_obj obj
310
+ end
311
+ end
312
+
313
+ # Write out traits
314
+ if traits[:class_name] && @trait_cache[traits] != nil
315
+ @stream << pack_integer(@trait_cache[traits] << 2 | 0x01)
316
+ else
317
+ @trait_cache.add_obj traits if traits[:class_name]
318
+
319
+ # Write out trait header
320
+ header = 0x03 # Not object ref and not trait ref
321
+ header |= 0x02 << 2 if traits[:dynamic]
322
+ header |= 0x01 << 2 if traits[:externalizable]
323
+ header |= traits[:members].length << 4
324
+ @stream << pack_integer(header)
325
+
326
+ # Write out class name
327
+ write_utf8_vr(traits[:class_name].to_s)
328
+
329
+ # Write out members
330
+ traits[:members].each {|m| write_utf8_vr(m)}
331
+ end
332
+
333
+ # If externalizable, take externalized data shortcut
334
+ if traits[:externalizable]
335
+ serialize obj.externalized_data
336
+ return
337
+ end
338
+
339
+ # Write out sealed properties
340
+ traits[:members].each do |m|
341
+ serialize props[m]
342
+ props.delete(m)
343
+ end
344
+
345
+ if traits[:dynamic]
346
+ # Write out dynamic properties
347
+ props.sort.each do |key, val| # Sort props until Ruby 1.9 becomes common
348
+ write_utf8_vr key.to_s
349
+ serialize val
350
+ end
351
+
352
+ # Write close
353
+ @stream << AMF3_CLOSE_DYNAMIC_OBJECT
354
+ end
355
+ end
356
+
357
+ private
358
+ include RocketAMF::Pure::WriteIOHelpers
359
+
360
+ def write_utf8_vr str
361
+ str = str.encode("UTF-8").force_encoding("ASCII-8BIT") if str.respond_to?(:encode)
362
+
363
+ if str == ''
364
+ @stream << AMF3_EMPTY_STRING
365
+ elsif @string_cache[str] != nil
366
+ write_reference @string_cache[str]
367
+ else
368
+ # Cache string
369
+ @string_cache.add_obj str
370
+
371
+ # Build AMF string
372
+ header = str.bytesize << 1 # make room for a low bit of 1
373
+ header = header | 1 # set the low bit to 1
374
+ @stream << pack_integer(header)
375
+ @stream << str
376
+ end
377
+ end
378
+ end
379
+
380
+ class SerializerCache #:nodoc:
381
+ def self.new type
382
+ if type == :string
383
+ StringCache.new
384
+ elsif type == :object
385
+ ObjectCache.new
386
+ elsif type == :trait
387
+ TraitCache.new
388
+ end
389
+ end
390
+
391
+ class StringCache < Hash #:nodoc:
392
+ def initialize
393
+ @cache_index = 0
394
+ end
395
+
396
+ def add_obj str
397
+ self[str] = @cache_index
398
+ @cache_index += 1
399
+ end
400
+ end
401
+
402
+ class ObjectCache < Hash #:nodoc:
403
+ def initialize
404
+ @cache_index = 0
405
+ end
406
+
407
+ def [] obj
408
+ super(obj.object_id)
409
+ end
410
+
411
+ def add_obj obj
412
+ self[obj.object_id] = @cache_index
413
+ @cache_index += 1
414
+ end
415
+ end
416
+
417
+ class TraitCache < Hash #:nodoc:
418
+ def initialize
419
+ @cache_index = 0
420
+ end
421
+
422
+ def [] obj
423
+ super(obj[:class_name])
424
+ end
425
+
426
+ def add_obj obj
427
+ self[obj[:class_name]] = @cache_index
428
+ @cache_index += 1
429
+ end
430
+ end
431
+ end
432
+ end
433
+ end