rocketamf_pure 1.0.0

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.
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