scotttam-RocketAMF 0.2.2

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