rocketamf_pure 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (91) hide show
  1. data/README.rdoc +47 -0
  2. data/Rakefile +9 -0
  3. data/benchmark.rb +73 -0
  4. data/lib/rocketamf.rb +212 -0
  5. data/lib/rocketamf/class_mapping.rb +237 -0
  6. data/lib/rocketamf/constants.rb +46 -0
  7. data/lib/rocketamf/ext.rb +28 -0
  8. data/lib/rocketamf/extensions.rb +22 -0
  9. data/lib/rocketamf/pure.rb +24 -0
  10. data/lib/rocketamf/pure/deserializer.rb +417 -0
  11. data/lib/rocketamf/pure/io_helpers.rb +94 -0
  12. data/lib/rocketamf/pure/remoting.rb +117 -0
  13. data/lib/rocketamf/pure/serializer.rb +474 -0
  14. data/lib/rocketamf/remoting.rb +196 -0
  15. data/lib/rocketamf/values/messages.rb +212 -0
  16. data/lib/rocketamf/values/typed_hash.rb +13 -0
  17. data/lib/rocketamf_pure.rb +1 -0
  18. data/spec/class_mapping_spec.rb +110 -0
  19. data/spec/deserializer_spec.rb +423 -0
  20. data/spec/fast_class_mapping_spec.rb +144 -0
  21. data/spec/fixtures/objects/amf0-boolean.bin +1 -0
  22. data/spec/fixtures/objects/amf0-complex-encoded-string.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-time.bin +0 -0
  33. data/spec/fixtures/objects/amf0-typed-object.bin +0 -0
  34. data/spec/fixtures/objects/amf0-undefined.bin +1 -0
  35. data/spec/fixtures/objects/amf0-untyped-object.bin +0 -0
  36. data/spec/fixtures/objects/amf0-xml-doc.bin +0 -0
  37. data/spec/fixtures/objects/amf3-0.bin +0 -0
  38. data/spec/fixtures/objects/amf3-array-collection.bin +2 -0
  39. data/spec/fixtures/objects/amf3-array-ref.bin +1 -0
  40. data/spec/fixtures/objects/amf3-associative-array.bin +1 -0
  41. data/spec/fixtures/objects/amf3-bigNum.bin +0 -0
  42. data/spec/fixtures/objects/amf3-byte-array-ref.bin +1 -0
  43. data/spec/fixtures/objects/amf3-byte-array.bin +0 -0
  44. data/spec/fixtures/objects/amf3-complex-array-collection.bin +6 -0
  45. data/spec/fixtures/objects/amf3-complex-encoded-string-array.bin +1 -0
  46. data/spec/fixtures/objects/amf3-date-ref.bin +0 -0
  47. data/spec/fixtures/objects/amf3-date.bin +0 -0
  48. data/spec/fixtures/objects/amf3-dictionary.bin +0 -0
  49. data/spec/fixtures/objects/amf3-dynamic-object.bin +2 -0
  50. data/spec/fixtures/objects/amf3-empty-array-ref.bin +1 -0
  51. data/spec/fixtures/objects/amf3-empty-array.bin +1 -0
  52. data/spec/fixtures/objects/amf3-empty-dictionary.bin +0 -0
  53. data/spec/fixtures/objects/amf3-empty-string-ref.bin +1 -0
  54. data/spec/fixtures/objects/amf3-encoded-string-ref.bin +0 -0
  55. data/spec/fixtures/objects/amf3-externalizable.bin +0 -0
  56. data/spec/fixtures/objects/amf3-false.bin +1 -0
  57. data/spec/fixtures/objects/amf3-float.bin +0 -0
  58. data/spec/fixtures/objects/amf3-graph-member.bin +0 -0
  59. data/spec/fixtures/objects/amf3-hash.bin +2 -0
  60. data/spec/fixtures/objects/amf3-large-max.bin +0 -0
  61. data/spec/fixtures/objects/amf3-large-min.bin +0 -0
  62. data/spec/fixtures/objects/amf3-max.bin +1 -0
  63. data/spec/fixtures/objects/amf3-min.bin +0 -0
  64. data/spec/fixtures/objects/amf3-mixed-array.bin +10 -0
  65. data/spec/fixtures/objects/amf3-null.bin +1 -0
  66. data/spec/fixtures/objects/amf3-object-ref.bin +0 -0
  67. data/spec/fixtures/objects/amf3-primitive-array.bin +1 -0
  68. data/spec/fixtures/objects/amf3-string-ref.bin +0 -0
  69. data/spec/fixtures/objects/amf3-string.bin +1 -0
  70. data/spec/fixtures/objects/amf3-symbol.bin +1 -0
  71. data/spec/fixtures/objects/amf3-trait-ref.bin +3 -0
  72. data/spec/fixtures/objects/amf3-true.bin +1 -0
  73. data/spec/fixtures/objects/amf3-typed-object.bin +2 -0
  74. data/spec/fixtures/objects/amf3-xml-doc.bin +1 -0
  75. data/spec/fixtures/objects/amf3-xml-ref.bin +1 -0
  76. data/spec/fixtures/objects/amf3-xml.bin +1 -0
  77. data/spec/fixtures/request/acknowledge-response.bin +0 -0
  78. data/spec/fixtures/request/amf0-error-response.bin +0 -0
  79. data/spec/fixtures/request/blaze-response.bin +0 -0
  80. data/spec/fixtures/request/commandMessage.bin +0 -0
  81. data/spec/fixtures/request/flex-request.bin +0 -0
  82. data/spec/fixtures/request/multiple-simple-request.bin +0 -0
  83. data/spec/fixtures/request/remotingMessage.bin +0 -0
  84. data/spec/fixtures/request/simple-request.bin +0 -0
  85. data/spec/fixtures/request/simple-response.bin +0 -0
  86. data/spec/fixtures/request/unsupportedCommandMessage.bin +0 -0
  87. data/spec/messages_spec.rb +39 -0
  88. data/spec/remoting_spec.rb +196 -0
  89. data/spec/serializer_spec.rb +503 -0
  90. data/spec/spec_helper.rb +55 -0
  91. metadata +148 -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,117 @@
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, class_mapper=nil
11
+ stream = StringIO.new(stream) unless StringIO === stream
12
+ des = Deserializer.new(class_mapper || RocketAMF::ClassMapper.new)
13
+ des.source = stream
14
+
15
+ # Initialize
16
+ @amf_version = 0
17
+ @headers = {}
18
+ @messages = []
19
+
20
+ # Read AMF version
21
+ @amf_version = read_word16_network stream
22
+
23
+ # Read in headers
24
+ header_count = read_word16_network stream
25
+ 0.upto(header_count-1) do
26
+ name = stream.read(read_word16_network(stream))
27
+ name.force_encoding("UTF-8") if name.respond_to?(:force_encoding)
28
+
29
+ must_understand = read_int8(stream) != 0
30
+
31
+ length = read_word32_network stream
32
+ data = des.deserialize(0, nil)
33
+
34
+ @headers[name] = RocketAMF::Header.new(name, must_understand, data)
35
+ end
36
+
37
+ # Read in messages
38
+ message_count = read_word16_network stream
39
+ 0.upto(message_count-1) do
40
+ target_uri = stream.read(read_word16_network(stream))
41
+ target_uri.force_encoding("UTF-8") if target_uri.respond_to?(:force_encoding)
42
+
43
+ response_uri = stream.read(read_word16_network(stream))
44
+ response_uri.force_encoding("UTF-8") if response_uri.respond_to?(:force_encoding)
45
+
46
+ length = read_word32_network stream
47
+ data = des.deserialize(0, nil)
48
+ if data.is_a?(Array) && data.length == 1 && data[0].is_a?(::RocketAMF::Values::AbstractMessage)
49
+ data = data[0]
50
+ end
51
+
52
+ @messages << RocketAMF::Message.new(target_uri, response_uri, data)
53
+ end
54
+
55
+ self
56
+ end
57
+
58
+ # Included into RocketAMF::Envelope, this method handles serializing an
59
+ # AMF request/response into a string
60
+ def serialize class_mapper=nil
61
+ ser = Serializer.new(class_mapper || RocketAMF::ClassMapper.new)
62
+ stream = ser.stream
63
+
64
+ # Write version
65
+ stream << pack_int16_network(@amf_version)
66
+
67
+ # Write headers
68
+ stream << pack_int16_network(@headers.length) # Header count
69
+ @headers.each_value do |h|
70
+ # Write header name
71
+ name_str = h.name
72
+ name_str.encode!("UTF-8").force_encoding("ASCII-8BIT") if name_str.respond_to?(:encode)
73
+ stream << pack_int16_network(name_str.bytesize)
74
+ stream << name_str
75
+
76
+ # Write must understand flag
77
+ stream << pack_int8(h.must_understand ? 1 : 0)
78
+
79
+ # Serialize data
80
+ stream << pack_word32_network(-1) # length of data - -1 if you don't know
81
+ ser.serialize(0, h.data)
82
+ end
83
+
84
+ # Write messages
85
+ stream << pack_int16_network(@messages.length) # Message count
86
+ @messages.each do |m|
87
+ # Write target_uri
88
+ uri_str = m.target_uri
89
+ uri_str.encode!("UTF-8").force_encoding("ASCII-8BIT") if uri_str.respond_to?(:encode)
90
+ stream << pack_int16_network(uri_str.bytesize)
91
+ stream << uri_str
92
+
93
+ # Write response_uri
94
+ uri_str = m.response_uri
95
+ uri_str.encode!("UTF-8").force_encoding("ASCII-8BIT") if uri_str.respond_to?(:encode)
96
+ stream << pack_int16_network(uri_str.bytesize)
97
+ stream << uri_str
98
+
99
+ # Serialize data
100
+ stream << pack_word32_network(-1) # length of data - -1 if you don't know
101
+ if @amf_version == 3
102
+ stream << AMF0_AMF3_MARKER
103
+ ser.serialize(3, m.data)
104
+ else
105
+ ser.serialize(0, m.data)
106
+ end
107
+ end
108
+
109
+ stream
110
+ end
111
+
112
+ private
113
+ include RocketAMF::Pure::ReadIOHelpers
114
+ include RocketAMF::Pure::WriteIOHelpers
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,474 @@
1
+ require 'rocketamf/pure/io_helpers'
2
+
3
+ module RocketAMF
4
+ module Pure
5
+ # Pure ruby serializer for AMF0 and AMF3
6
+ class Serializer
7
+ attr_reader :stream, :version
8
+
9
+ # Pass in the class mapper instance to use when serializing. This enables
10
+ # better caching behavior in the class mapper and allows one to change
11
+ # mappings between serialization attempts.
12
+ def initialize class_mapper
13
+ @class_mapper = class_mapper
14
+ @stream = ""
15
+ @depth = 0
16
+ end
17
+
18
+ # Serialize the given object using AMF0 or AMF3. Can be called from inside
19
+ # encode_amf, but make sure to pass in the proper version or it may not be
20
+ # possible to decode. Use the serializer version attribute for this.
21
+ def serialize version, obj
22
+ raise ArgumentError, "unsupported version #{version}" unless [0,3].include?(version)
23
+ @version = version
24
+
25
+ # Initialize caches
26
+ if @depth == 0
27
+ if @version == 0
28
+ @ref_cache = SerializerCache.new :object
29
+ else
30
+ @string_cache = SerializerCache.new :string
31
+ @object_cache = SerializerCache.new :object
32
+ @trait_cache = SerializerCache.new :string
33
+ end
34
+ end
35
+ @depth += 1
36
+
37
+ # Perform serialization
38
+ if @version == 0
39
+ amf0_serialize(obj)
40
+ else
41
+ amf3_serialize(obj)
42
+ end
43
+
44
+ # Cleanup
45
+ @depth -= 1
46
+ if @depth == 0
47
+ @ref_cache = nil
48
+ @string_cache = nil
49
+ @object_cache = nil
50
+ @trait_cache = nil
51
+ end
52
+
53
+ return @stream
54
+ end
55
+
56
+ # Helper for writing arrays inside encode_amf. It uses the current AMF
57
+ # version to write the array.
58
+ def write_array arr
59
+ if @version == 0
60
+ amf0_write_array arr
61
+ else
62
+ amf3_write_array arr
63
+ end
64
+ end
65
+
66
+ # Helper for writing objects inside encode_amf. It uses the current AMF
67
+ # version to write the object. If you pass in a property hash, it will use
68
+ # it rather than having the class mapper determine properties. For AMF3,
69
+ # you can also specify a traits hash, which can be used to reduce serialized
70
+ # data size or serialize things as externalizable.
71
+ def write_object obj, props=nil, traits=nil
72
+ if @version == 0
73
+ amf0_write_object obj, props
74
+ else
75
+ amf3_write_object obj, props, traits
76
+ end
77
+ end
78
+
79
+ private
80
+ include RocketAMF::Pure::WriteIOHelpers
81
+
82
+ def amf0_serialize obj
83
+ if @ref_cache[obj] != nil
84
+ amf0_write_reference @ref_cache[obj]
85
+ elsif obj.respond_to?(:encode_amf)
86
+ obj.encode_amf(self)
87
+ elsif obj.is_a?(NilClass)
88
+ amf0_write_null
89
+ elsif obj.is_a?(TrueClass) || obj.is_a?(FalseClass)
90
+ amf0_write_boolean obj
91
+ elsif obj.is_a?(Numeric)
92
+ amf0_write_number obj
93
+ elsif obj.is_a?(Symbol) || obj.is_a?(String)
94
+ amf0_write_string obj.to_s
95
+ elsif obj.is_a?(Time)
96
+ amf0_write_time obj
97
+ elsif obj.is_a?(Date)
98
+ amf0_write_date obj
99
+ elsif obj.is_a?(Array)
100
+ amf0_write_array obj
101
+ elsif obj.is_a?(Hash) ||obj.is_a?(Object)
102
+ amf0_write_object obj
103
+ end
104
+ end
105
+
106
+ def amf0_write_null
107
+ @stream << AMF0_NULL_MARKER
108
+ end
109
+
110
+ def amf0_write_boolean bool
111
+ @stream << AMF0_BOOLEAN_MARKER
112
+ @stream << pack_int8(bool ? 1 : 0)
113
+ end
114
+
115
+ def amf0_write_number num
116
+ @stream << AMF0_NUMBER_MARKER
117
+ @stream << pack_double(num)
118
+ end
119
+
120
+ def amf0_write_string str
121
+ str = str.encode("UTF-8").force_encoding("ASCII-8BIT") if str.respond_to?(:encode)
122
+ len = str.bytesize
123
+ if len > 2**16-1
124
+ @stream << AMF0_LONG_STRING_MARKER
125
+ @stream << pack_word32_network(len)
126
+ else
127
+ @stream << AMF0_STRING_MARKER
128
+ @stream << pack_int16_network(len)
129
+ end
130
+ @stream << str
131
+ end
132
+
133
+ def amf0_write_time time
134
+ @stream << AMF0_DATE_MARKER
135
+
136
+ time = time.getutc # Dup and convert to UTC
137
+ milli = (time.to_f * 1000).to_i
138
+ @stream << pack_double(milli)
139
+
140
+ @stream << pack_int16_network(0) # Time zone
141
+ end
142
+
143
+ def amf0_write_date date
144
+ @stream << AMF0_DATE_MARKER
145
+ @stream << pack_double(date.strftime("%Q").to_i)
146
+ @stream << pack_int16_network(0) # Time zone
147
+ end
148
+
149
+ def amf0_write_reference index
150
+ @stream << AMF0_REFERENCE_MARKER
151
+ @stream << pack_int16_network(index)
152
+ end
153
+
154
+ def amf0_write_array array
155
+ @ref_cache.add_obj array
156
+ @stream << AMF0_STRICT_ARRAY_MARKER
157
+ @stream << pack_word32_network(array.length)
158
+ array.each do |elem|
159
+ amf0_serialize elem
160
+ end
161
+ end
162
+
163
+ def amf0_write_object obj, props=nil
164
+ @ref_cache.add_obj obj
165
+
166
+ props = @class_mapper.props_for_serialization obj if props.nil?
167
+
168
+ # Is it a typed object?
169
+ class_name = @class_mapper.get_as_class_name obj
170
+ if class_name
171
+ class_name = class_name.encode("UTF-8").force_encoding("ASCII-8BIT") if class_name.respond_to?(:encode)
172
+ @stream << AMF0_TYPED_OBJECT_MARKER
173
+ @stream << pack_int16_network(class_name.bytesize)
174
+ @stream << class_name
175
+ else
176
+ @stream << AMF0_OBJECT_MARKER
177
+ end
178
+
179
+ # Write prop list
180
+ props.sort.each do |key, value| # Sort keys before writing
181
+ key = key.encode("UTF-8").force_encoding("ASCII-8BIT") if key.respond_to?(:encode)
182
+ @stream << pack_int16_network(key.bytesize)
183
+ @stream << key
184
+ amf0_serialize value
185
+ end
186
+
187
+ # Write end
188
+ @stream << pack_int16_network(0)
189
+ @stream << AMF0_OBJECT_END_MARKER
190
+ end
191
+
192
+ def amf3_serialize obj
193
+ if obj.respond_to?(:encode_amf)
194
+ obj.encode_amf(self)
195
+ elsif obj.is_a?(NilClass)
196
+ amf3_write_null
197
+ elsif obj.is_a?(TrueClass)
198
+ amf3_write_true
199
+ elsif obj.is_a?(FalseClass)
200
+ amf3_write_false
201
+ elsif obj.is_a?(Numeric)
202
+ amf3_write_numeric obj
203
+ elsif obj.is_a?(Symbol) || obj.is_a?(String)
204
+ amf3_write_string obj.to_s
205
+ elsif obj.is_a?(Time)
206
+ amf3_write_time obj
207
+ elsif obj.is_a?(Date)
208
+ amf3_write_date obj
209
+ elsif obj.is_a?(StringIO)
210
+ amf3_write_byte_array obj
211
+ elsif obj.is_a?(Array)
212
+ amf3_write_array obj
213
+ elsif obj.is_a?(Hash) || obj.is_a?(Object)
214
+ amf3_write_object obj
215
+ end
216
+ end
217
+
218
+ def amf3_write_reference index
219
+ header = index << 1 # shift value left to leave a low bit of 0
220
+ @stream << pack_integer(header)
221
+ end
222
+
223
+ def amf3_write_null
224
+ @stream << AMF3_NULL_MARKER
225
+ end
226
+
227
+ def amf3_write_true
228
+ @stream << AMF3_TRUE_MARKER
229
+ end
230
+
231
+ def amf3_write_false
232
+ @stream << AMF3_FALSE_MARKER
233
+ end
234
+
235
+ def amf3_write_numeric num
236
+ if !num.integer? || num < MIN_INTEGER || num > MAX_INTEGER # Check valid range for 29 bits
237
+ @stream << AMF3_DOUBLE_MARKER
238
+ @stream << pack_double(num)
239
+ else
240
+ @stream << AMF3_INTEGER_MARKER
241
+ @stream << pack_integer(num)
242
+ end
243
+ end
244
+
245
+ def amf3_write_string str
246
+ @stream << AMF3_STRING_MARKER
247
+ amf3_write_utf8_vr str
248
+ end
249
+
250
+ def amf3_write_time time
251
+ @stream << AMF3_DATE_MARKER
252
+ if @object_cache[time] != nil
253
+ amf3_write_reference @object_cache[time]
254
+ else
255
+ # Cache time
256
+ @object_cache.add_obj time
257
+
258
+ # Build AMF string
259
+ time = time.getutc # Dup and convert to UTC
260
+ milli = (time.to_f * 1000).to_i
261
+ @stream << AMF3_NULL_MARKER
262
+ @stream << pack_double(milli)
263
+ end
264
+ end
265
+
266
+ def amf3_write_date date
267
+ @stream << AMF3_DATE_MARKER
268
+ if @object_cache[date] != nil
269
+ amf3_write_reference @object_cache[date]
270
+ else
271
+ # Cache date
272
+ @object_cache.add_obj date
273
+
274
+ # Build AMF string
275
+ @stream << AMF3_NULL_MARKER
276
+ @stream << pack_double(date.strftime("%Q").to_i)
277
+ end
278
+ end
279
+
280
+ def amf3_write_byte_array array
281
+ @stream << AMF3_BYTE_ARRAY_MARKER
282
+ if @object_cache[array] != nil
283
+ amf3_write_reference @object_cache[array]
284
+ else
285
+ @object_cache.add_obj array
286
+ str = array.string
287
+ @stream << pack_integer(str.bytesize << 1 | 1)
288
+ @stream << str
289
+ end
290
+ end
291
+
292
+ def amf3_write_array array
293
+ # Is it an array collection?
294
+ is_ac = false
295
+ if array.respond_to?(:is_array_collection?)
296
+ is_ac = array.is_array_collection?
297
+ else
298
+ is_ac = @class_mapper.use_array_collection
299
+ end
300
+
301
+ # Write type marker
302
+ @stream << (is_ac ? AMF3_OBJECT_MARKER : AMF3_ARRAY_MARKER)
303
+
304
+ # Write reference or cache array
305
+ if @object_cache[array] != nil
306
+ amf3_write_reference @object_cache[array]
307
+ return
308
+ else
309
+ @object_cache.add_obj array
310
+ @object_cache.add_obj nil if is_ac # The array collection source array
311
+ end
312
+
313
+ # Write out traits and array marker if it's an array collection
314
+ if is_ac
315
+ class_name = "flex.messaging.io.ArrayCollection"
316
+ if @trait_cache[class_name] != nil
317
+ @stream << pack_integer(@trait_cache[class_name] << 2 | 0x01)
318
+ else
319
+ @trait_cache.add_obj class_name
320
+ @stream << "\a" # Externalizable, non-dynamic
321
+ amf3_write_utf8_vr(class_name)
322
+ end
323
+ @stream << AMF3_ARRAY_MARKER
324
+ end
325
+
326
+ # Build AMF string for array
327
+ header = array.length << 1 # make room for a low bit of 1
328
+ header = header | 1 # set the low bit to 1
329
+ @stream << pack_integer(header)
330
+ @stream << AMF3_CLOSE_DYNAMIC_ARRAY
331
+ array.each do |elem|
332
+ amf3_serialize elem
333
+ end
334
+ end
335
+
336
+ def amf3_write_object obj, props=nil, traits=nil
337
+ @stream << AMF3_OBJECT_MARKER
338
+
339
+ # Caching...
340
+ if @object_cache[obj] != nil
341
+ amf3_write_reference @object_cache[obj]
342
+ return
343
+ end
344
+ @object_cache.add_obj obj
345
+
346
+ # Calculate traits if not given
347
+ is_default = false
348
+ if traits.nil?
349
+ traits = {
350
+ :class_name => @class_mapper.get_as_class_name(obj),
351
+ :members => [],
352
+ :externalizable => false,
353
+ :dynamic => true
354
+ }
355
+ is_default = true unless traits[:class_name]
356
+ end
357
+ class_name = is_default ? "__default__" : traits[:class_name]
358
+
359
+ # Write out traits
360
+ if (class_name && @trait_cache[class_name] != nil)
361
+ @stream << pack_integer(@trait_cache[class_name] << 2 | 0x01)
362
+ else
363
+ @trait_cache.add_obj class_name if class_name
364
+
365
+ # Write out trait header
366
+ header = 0x03 # Not object ref and not trait ref
367
+ header |= 0x02 << 2 if traits[:dynamic]
368
+ header |= 0x01 << 2 if traits[:externalizable]
369
+ header |= traits[:members].length << 4
370
+ @stream << pack_integer(header)
371
+
372
+ # Write out class name
373
+ if class_name == "__default__"
374
+ amf3_write_utf8_vr("")
375
+ else
376
+ amf3_write_utf8_vr(class_name.to_s)
377
+ end
378
+
379
+ # Write out members
380
+ traits[:members].each {|m| amf3_write_utf8_vr(m)}
381
+ end
382
+
383
+ # If externalizable, take externalized data shortcut
384
+ if traits[:externalizable]
385
+ obj.write_external(self)
386
+ return
387
+ end
388
+
389
+ # Extract properties if not given
390
+ props = @class_mapper.props_for_serialization(obj) if props.nil?
391
+
392
+ # Write out sealed properties
393
+ traits[:members].each do |m|
394
+ amf3_serialize props[m]
395
+ props.delete(m)
396
+ end
397
+
398
+ # Write out dynamic properties
399
+ if traits[:dynamic]
400
+ # Write out dynamic properties
401
+ props.sort.each do |key, val| # Sort props until Ruby 1.9 becomes common
402
+ amf3_write_utf8_vr key.to_s
403
+ amf3_serialize val
404
+ end
405
+
406
+ # Write close
407
+ @stream << AMF3_CLOSE_DYNAMIC_OBJECT
408
+ end
409
+ end
410
+
411
+ def amf3_write_utf8_vr str, encode=true
412
+ if str.respond_to?(:encode)
413
+ if encode
414
+ str = str.encode("UTF-8")
415
+ else
416
+ str = str.dup if str.frozen?
417
+ end
418
+ str.force_encoding("ASCII-8BIT")
419
+ end
420
+
421
+ if str == ''
422
+ @stream << AMF3_EMPTY_STRING
423
+ elsif @string_cache[str] != nil
424
+ amf3_write_reference @string_cache[str]
425
+ else
426
+ # Cache string
427
+ @string_cache.add_obj str
428
+
429
+ # Build AMF string
430
+ @stream << pack_integer(str.bytesize << 1 | 1)
431
+ @stream << str
432
+ end
433
+ end
434
+ end
435
+
436
+ class SerializerCache #:nodoc:
437
+ def self.new type
438
+ if type == :string
439
+ StringCache.new
440
+ elsif type == :object
441
+ ObjectCache.new
442
+ end
443
+ end
444
+
445
+ class StringCache < Hash #:nodoc:
446
+ def initialize
447
+ @cache_index = 0
448
+ end
449
+
450
+ def add_obj str
451
+ self[str] = @cache_index
452
+ @cache_index += 1
453
+ end
454
+ end
455
+
456
+ class ObjectCache < Hash #:nodoc:
457
+ def initialize
458
+ @cache_index = 0
459
+ @obj_references = []
460
+ end
461
+
462
+ def [] obj
463
+ super(obj.object_id)
464
+ end
465
+
466
+ def add_obj obj
467
+ @obj_references << obj
468
+ self[obj.object_id] = @cache_index
469
+ @cache_index += 1
470
+ end
471
+ end
472
+ end
473
+ end
474
+ end