├── spec ├── fixtures │ ├── objects │ │ ├── amf0-null.bin │ │ ├── amf3-0.bin │ │ ├── amf3-false.bin │ │ ├── amf3-null.bin │ │ ├── amf3-true.bin │ │ ├── amf0-boolean.bin │ │ ├── amf0-undefined.bin │ │ ├── amf3-symbol.bin │ │ ├── amf0-number.bin │ │ ├── amf3-bigNum.bin │ │ ├── amf3-date.bin │ │ ├── amf3-empty-array.bin │ │ ├── amf3-float.bin │ │ ├── amf0-string.bin │ │ ├── amf3-byte-array.bin │ │ ├── amf3-empty-dictionary.bin │ │ ├── amf0-hash.bin │ │ ├── amf3-byte-array-ref.bin │ │ ├── amf3-date-ref.bin │ │ ├── amf3-empty-string-ref.bin │ │ ├── amf3-string.bin │ │ ├── amf3-vector-uint.bin │ │ ├── amf3-empty-array-ref.bin │ │ ├── amf3-hash.bin │ │ ├── amf3-primitive-array.bin │ │ ├── amf0-strict-array.bin │ │ ├── amf0-untyped-object.bin │ │ ├── amf3-array-ref.bin │ │ ├── amf3-encoded-string-ref.bin │ │ ├── amf3-string-ref.bin │ │ ├── amf3-xml.bin │ │ ├── amf3-xml-doc.bin │ │ ├── amf0-empty-string-key-hash.bin │ │ ├── amf0-typed-object.bin │ │ ├── amf0-xml-doc.bin │ │ ├── amf3-typed-object.bin │ │ ├── amf3-xml-ref.bin │ │ ├── amf0-ecma-ordinal-array.bin │ │ ├── amf3-complex-encoded-string-array.bin │ │ ├── amf3-trait-ref.bin │ │ ├── amf3-object-ref.bin │ │ ├── amf3-associative-array.bin │ │ ├── amf3-dictionary.bin │ │ ├── amf3-array-collection.bin │ │ ├── amf3-vector-object.bin │ │ ├── amf0-complex-encoded-string.bin │ │ ├── amf3-graph-member.bin │ │ ├── amf3-externalizable.bin │ │ ├── amf0-date.bin │ │ ├── amf0-time.bin │ │ ├── amf3-max.bin │ │ ├── amf3-min.bin │ │ ├── amf0-object.bin │ │ ├── amf3-dynamic-object.bin │ │ ├── amf0-ref-test.bin │ │ ├── amf3-large-max.bin │ │ ├── amf3-large-min.bin │ │ ├── amf3-vector-int.bin │ │ ├── amf3-vector-double.bin │ │ ├── amf3-complex-array-collection.bin │ │ └── amf3-mixed-array.bin │ └── request │ │ ├── blaze-response.bin │ │ ├── commandMessage.bin │ │ ├── flex-request.bin │ │ ├── simple-request.bin │ │ ├── remotingMessage.bin │ │ ├── simple-response.bin │ │ ├── amf0-error-response.bin │ │ ├── acknowledge-response.bin │ │ ├── multiple-simple-request.bin │ │ └── unsupportedCommandMessage.bin ├── flash │ ├── build │ ├── ASClass.as │ ├── encoder-app.xml │ ├── ExternalizableTest.as │ └── Encoder.as ├── messages_spec.rb ├── spec_helper.rb ├── class_mapping_spec.rb ├── fast_class_mapping_spec.rb ├── remoting_spec.rb ├── deserializer_spec.rb └── serializer_spec.rb ├── .gitignore ├── ext └── rocketamf_ext │ ├── utility.h │ ├── extconf.rb │ ├── serializer.h │ ├── deserializer.h │ ├── rocketamf_ext.c │ ├── constants.h │ ├── remoting.c │ ├── class_mapping.c │ └── deserializer.c ├── lib ├── rocketamf │ ├── values │ │ ├── typed_hash.rb │ │ └── messages.rb │ ├── extensions.rb │ ├── pure.rb │ ├── ext.rb │ ├── constants.rb │ ├── pure │ │ ├── io_helpers.rb │ │ ├── remoting.rb │ │ ├── deserializer.rb │ │ └── serializer.rb │ ├── remoting.rb │ └── class_mapping.rb └── rocketamf.rb ├── RocketAMF.gemspec ├── Rakefile ├── README.rdoc └── benchmark.rb /spec/fixtures/objects/amf0-null.bin: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /spec/fixtures/objects/amf3-0.bin: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /spec/fixtures/objects/amf3-false.bin: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /spec/fixtures/objects/amf3-null.bin: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /spec/fixtures/objects/amf3-true.bin: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /spec/fixtures/objects/amf0-boolean.bin: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /spec/fixtures/objects/amf0-undefined.bin: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /spec/fixtures/objects/amf3-symbol.bin: -------------------------------------------------------------------------------- 1 | foo -------------------------------------------------------------------------------- /spec/fixtures/objects/amf0-number.bin: -------------------------------------------------------------------------------- 1 | @ -------------------------------------------------------------------------------- /spec/fixtures/objects/amf3-bigNum.bin: -------------------------------------------------------------------------------- 1 | ~p -------------------------------------------------------------------------------- /spec/fixtures/objects/amf3-date.bin: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /spec/fixtures/objects/amf3-empty-array.bin: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /spec/fixtures/objects/amf3-float.bin: -------------------------------------------------------------------------------- 1 | @ -------------------------------------------------------------------------------- /spec/fixtures/objects/amf0-string.bin: -------------------------------------------------------------------------------- 1 | this is a テスト -------------------------------------------------------------------------------- /spec/fixtures/objects/amf3-byte-array.bin: -------------------------------------------------------------------------------- 1 | これtest@ -------------------------------------------------------------------------------- /spec/fixtures/objects/amf3-empty-dictionary.bin: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /spec/fixtures/objects/amf0-hash.bin: -------------------------------------------------------------------------------- 1 | abcd -------------------------------------------------------------------------------- /spec/fixtures/objects/amf3-byte-array-ref.bin: -------------------------------------------------------------------------------- 1 |  ASDF  -------------------------------------------------------------------------------- /spec/fixtures/objects/amf3-date-ref.bin: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /spec/fixtures/objects/amf3-empty-string-ref.bin: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /spec/fixtures/objects/amf3-string.bin: -------------------------------------------------------------------------------- 1 | String . String -------------------------------------------------------------------------------- /spec/fixtures/objects/amf3-vector-uint.bin: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /spec/fixtures/objects/amf3-empty-array-ref.bin: -------------------------------------------------------------------------------- 1 |      -------------------------------------------------------------------------------- /spec/fixtures/objects/amf3-hash.bin: -------------------------------------------------------------------------------- 1 | 2 |  answer*foobar -------------------------------------------------------------------------------- /spec/fixtures/objects/amf3-primitive-array.bin: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /spec/fixtures/objects/amf0-strict-array.bin: -------------------------------------------------------------------------------- 1 | 2 | abcd -------------------------------------------------------------------------------- /spec/fixtures/objects/amf0-untyped-object.bin: -------------------------------------------------------------------------------- 1 | bazfoobar -------------------------------------------------------------------------------- /spec/fixtures/objects/amf3-array-ref.bin: -------------------------------------------------------------------------------- 1 |   abc   -------------------------------------------------------------------------------- /spec/fixtures/objects/amf3-encoded-string-ref.bin: -------------------------------------------------------------------------------- 1 | 'this is a テスト -------------------------------------------------------------------------------- /spec/fixtures/objects/amf3-string-ref.bin: -------------------------------------------------------------------------------- 1 | foostr 2 |  -------------------------------------------------------------------------------- /spec/fixtures/objects/amf3-xml.bin: -------------------------------------------------------------------------------- 1 | K -------------------------------------------------------------------------------- /spec/fixtures/objects/amf3-xml-doc.bin: -------------------------------------------------------------------------------- 1 | M -------------------------------------------------------------------------------- /spec/fixtures/objects/amf0-empty-string-key-hash.bin: -------------------------------------------------------------------------------- 1 | cdablast -------------------------------------------------------------------------------- /spec/fixtures/objects/amf0-typed-object.bin: -------------------------------------------------------------------------------- 1 | org.amf.ASClassbazfoobar -------------------------------------------------------------------------------- /spec/fixtures/objects/amf0-xml-doc.bin: -------------------------------------------------------------------------------- 1 | & -------------------------------------------------------------------------------- /spec/fixtures/objects/amf3-typed-object.bin: -------------------------------------------------------------------------------- 1 | 2 | #org.amf.ASClassbazfoobar -------------------------------------------------------------------------------- /spec/fixtures/objects/amf3-xml-ref.bin: -------------------------------------------------------------------------------- 1 |  K  -------------------------------------------------------------------------------- /spec/fixtures/objects/amf0-ecma-ordinal-array.bin: -------------------------------------------------------------------------------- 1 | 0a1b2c3d -------------------------------------------------------------------------------- /spec/fixtures/objects/amf3-complex-encoded-string-array.bin: -------------------------------------------------------------------------------- 1 | Shift テストUTF テスト -------------------------------------------------------------------------------- /spec/fixtures/objects/amf3-trait-ref.bin: -------------------------------------------------------------------------------- 1 |  2 | #org.amf.ASClassbazfoo 3 | bar -------------------------------------------------------------------------------- /spec/flash/build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | amxmlc Encoder.as -debug 3 | adl encoder-app.xml 4 | -------------------------------------------------------------------------------- /spec/fixtures/objects/amf3-object-ref.bin: -------------------------------------------------------------------------------- 1 |   2 | foobar 3 |   4 |  5 |  -------------------------------------------------------------------------------- /spec/fixtures/objects/amf3-associative-array.bin: -------------------------------------------------------------------------------- 1 |  asdf fdsafoobar42 bar1 bar2 bar3 -------------------------------------------------------------------------------- /spec/fixtures/objects/amf3-dictionary.bin: -------------------------------------------------------------------------------- 1 | bar asdf1 2 | #org.amf.ASClassbazfoo asdf2 -------------------------------------------------------------------------------- /spec/fixtures/objects/amf3-array-collection.bin: -------------------------------------------------------------------------------- 1 | 2 | Cflex.messaging.io.ArrayCollection foobar -------------------------------------------------------------------------------- /spec/fixtures/objects/amf3-vector-object.bin: -------------------------------------------------------------------------------- 1 | org.amf.ASClass 2 | #bazfoo 3 | bar 4 |  -------------------------------------------------------------------------------- /spec/fixtures/objects/amf0-complex-encoded-string.bin: -------------------------------------------------------------------------------- 1 | shiftShift テストutf UTF テストzed@ -------------------------------------------------------------------------------- /spec/fixtures/objects/amf3-graph-member.bin: -------------------------------------------------------------------------------- 1 | 2 | children  3 |   parent 4 |  5 |   6 |  -------------------------------------------------------------------------------- /spec/fixtures/objects/amf3-externalizable.bin: -------------------------------------------------------------------------------- 1 |  2 | %ExternalizableTest@@ 3 | @*@ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pkg/* 2 | .loadpath 3 | .project 4 | /rdoc/ 5 | *.bundle 6 | *.so 7 | *.dll 8 | /tmp 9 | *.swf 10 | -------------------------------------------------------------------------------- /spec/fixtures/objects/amf0-date.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubyamf/rocketamf/HEAD/spec/fixtures/objects/amf0-date.bin -------------------------------------------------------------------------------- /spec/fixtures/objects/amf0-time.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubyamf/rocketamf/HEAD/spec/fixtures/objects/amf0-time.bin -------------------------------------------------------------------------------- /spec/fixtures/objects/amf3-max.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubyamf/rocketamf/HEAD/spec/fixtures/objects/amf3-max.bin -------------------------------------------------------------------------------- /spec/fixtures/objects/amf3-min.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubyamf/rocketamf/HEAD/spec/fixtures/objects/amf3-min.bin -------------------------------------------------------------------------------- /spec/fixtures/objects/amf0-object.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubyamf/rocketamf/HEAD/spec/fixtures/objects/amf0-object.bin -------------------------------------------------------------------------------- /spec/fixtures/objects/amf3-dynamic-object.bin: -------------------------------------------------------------------------------- 1 | 2 | /another_public_propertya_public_valuenil_propertyproperty_onefoo -------------------------------------------------------------------------------- /spec/fixtures/objects/amf0-ref-test.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubyamf/rocketamf/HEAD/spec/fixtures/objects/amf0-ref-test.bin -------------------------------------------------------------------------------- /spec/fixtures/objects/amf3-large-max.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubyamf/rocketamf/HEAD/spec/fixtures/objects/amf3-large-max.bin -------------------------------------------------------------------------------- /spec/fixtures/objects/amf3-large-min.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubyamf/rocketamf/HEAD/spec/fixtures/objects/amf3-large-min.bin -------------------------------------------------------------------------------- /spec/fixtures/request/blaze-response.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubyamf/rocketamf/HEAD/spec/fixtures/request/blaze-response.bin -------------------------------------------------------------------------------- /spec/fixtures/request/commandMessage.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubyamf/rocketamf/HEAD/spec/fixtures/request/commandMessage.bin -------------------------------------------------------------------------------- /spec/fixtures/request/flex-request.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubyamf/rocketamf/HEAD/spec/fixtures/request/flex-request.bin -------------------------------------------------------------------------------- /spec/fixtures/request/simple-request.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubyamf/rocketamf/HEAD/spec/fixtures/request/simple-request.bin -------------------------------------------------------------------------------- /spec/fixtures/objects/amf3-vector-int.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubyamf/rocketamf/HEAD/spec/fixtures/objects/amf3-vector-int.bin -------------------------------------------------------------------------------- /spec/fixtures/request/remotingMessage.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubyamf/rocketamf/HEAD/spec/fixtures/request/remotingMessage.bin -------------------------------------------------------------------------------- /spec/fixtures/request/simple-response.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubyamf/rocketamf/HEAD/spec/fixtures/request/simple-response.bin -------------------------------------------------------------------------------- /spec/fixtures/objects/amf3-vector-double.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubyamf/rocketamf/HEAD/spec/fixtures/objects/amf3-vector-double.bin -------------------------------------------------------------------------------- /spec/fixtures/request/amf0-error-response.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubyamf/rocketamf/HEAD/spec/fixtures/request/amf0-error-response.bin -------------------------------------------------------------------------------- /spec/fixtures/request/acknowledge-response.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubyamf/rocketamf/HEAD/spec/fixtures/request/acknowledge-response.bin -------------------------------------------------------------------------------- /spec/fixtures/request/multiple-simple-request.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubyamf/rocketamf/HEAD/spec/fixtures/request/multiple-simple-request.bin -------------------------------------------------------------------------------- /spec/fixtures/request/unsupportedCommandMessage.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubyamf/rocketamf/HEAD/spec/fixtures/request/unsupportedCommandMessage.bin -------------------------------------------------------------------------------- /spec/fixtures/objects/amf3-complex-array-collection.bin: -------------------------------------------------------------------------------- 1 |  2 | Cflex.messaging.io.ArrayCollection foobar 3 |   4 | #org.amf.ASClassbaz 5 |  asdf 6 |  -------------------------------------------------------------------------------- /spec/fixtures/objects/amf3-mixed-array.bin: -------------------------------------------------------------------------------- 1 |  2 | foo_onebar_one 3 | foo_two 4 | foo_three* 5 |   6 |  7 |  8 |  *  9 |  10 |  -------------------------------------------------------------------------------- /ext/rocketamf_ext/utility.h: -------------------------------------------------------------------------------- 1 | // Before RFLOAT_VALUE, value was in a different place in the struct 2 | #ifndef RFLOAT_VALUE 3 | #define RFLOAT_VALUE(v) (RFLOAT(v)->value) 4 | #endif -------------------------------------------------------------------------------- /spec/flash/ASClass.as: -------------------------------------------------------------------------------- 1 | package { 2 | public class ASClass { 3 | public var baz:Object = null; 4 | public var foo:String = "bar"; 5 | public function ASClass($foo:String) { 6 | foo = $foo; 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /lib/rocketamf/values/typed_hash.rb: -------------------------------------------------------------------------------- 1 | module RocketAMF 2 | module Values #:nodoc: 3 | # Hash-like object that can store a type string. Used to preserve type information 4 | # for unmapped objects after deserialization. 5 | class TypedHash < Hash 6 | attr_reader :type 7 | 8 | def initialize type 9 | @type = type 10 | end 11 | end 12 | end 13 | end -------------------------------------------------------------------------------- /spec/flash/encoder-app.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | com.rubyamf.Encoder 4 | Encoder 5 | Encoder 6 | 1.0 7 | 8 | Encoder.swf 9 | standard 10 | false 11 | true 12 | 13 | 14 | -------------------------------------------------------------------------------- /spec/flash/ExternalizableTest.as: -------------------------------------------------------------------------------- 1 | package { 2 | import flash.utils.*; 3 | 4 | public class ExternalizableTest implements IExternalizable { 5 | private var one:int; 6 | private var two:int; 7 | 8 | public function ExternalizableTest(one:int, two:int) { 9 | this.one = one; 10 | this.two = two; 11 | } 12 | 13 | public function writeExternal(output:IDataOutput):void { 14 | output.writeDouble(one); 15 | output.writeDouble(two); 16 | } 17 | 18 | public function readExternal(input:IDataInput):void { 19 | one = input.readDouble(); 20 | two = input.readDouble(); 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /ext/rocketamf_ext/extconf.rb: -------------------------------------------------------------------------------- 1 | require 'mkmf' 2 | 3 | # Disable the native extension by creating an empty Makefile on JRuby 4 | if defined? JRUBY_VERSION 5 | message "Generating phony Makefile for JRuby so the gem installs" 6 | mfile = File.join(File.dirname(__FILE__), 'Makefile') 7 | File.open(mfile, 'w') {|f| f.write dummy_makefile(File.dirname(__FILE__)) } 8 | exit 0 9 | end 10 | 11 | if enable_config("sort-props", false) 12 | $defs.push("-DSORT_PROPS") unless $defs.include? "-DSORT_PROPS" 13 | end 14 | have_func('rb_str_encode') 15 | 16 | $CFLAGS += " -Wall" 17 | 18 | create_makefile('rocketamf_ext') -------------------------------------------------------------------------------- /lib/rocketamf/extensions.rb: -------------------------------------------------------------------------------- 1 | # Joc's monkeypatch for string bytesize (only available in 1.8.7+) 2 | if !"amf".respond_to? :bytesize 3 | class String #:nodoc: 4 | def bytesize 5 | self.size 6 | end 7 | end 8 | end 9 | 10 | # Add ArrayCollection override to arrays 11 | class Array 12 | # Override RocketAMF::ClassMapper.use_array_collection setting for 13 | # this array. Adds is_array_collection? method, which is used by the 14 | # serializer over the global config if defined. 15 | def is_array_collection= a 16 | @is_array_collection = a 17 | 18 | def self.is_array_collection? #:nodoc: 19 | @is_array_collection 20 | end 21 | end 22 | end -------------------------------------------------------------------------------- /lib/rocketamf/pure.rb: -------------------------------------------------------------------------------- 1 | require 'rocketamf/pure/deserializer' 2 | require 'rocketamf/pure/serializer' 3 | require 'rocketamf/pure/remoting' 4 | 5 | module RocketAMF 6 | # This module holds all the modules/classes that implement AMF's functionality 7 | # in pure ruby 8 | module Pure 9 | $DEBUG and warn "Using pure library for RocketAMF." 10 | end 11 | 12 | #:stopdoc: 13 | # Import serializer/deserializer 14 | Deserializer = RocketAMF::Pure::Deserializer 15 | Serializer = RocketAMF::Pure::Serializer 16 | 17 | # Modify envelope so it can serialize/deserialize 18 | class Envelope 19 | remove_method :populate_from_stream 20 | remove_method :serialize 21 | include RocketAMF::Pure::Envelope 22 | end 23 | #:startdoc: 24 | end -------------------------------------------------------------------------------- /lib/rocketamf/ext.rb: -------------------------------------------------------------------------------- 1 | begin 2 | # Fat binaries for Windows 3 | RUBY_VERSION =~ /(\d+.\d+)/ 4 | require "#{$1}/rocketamf_ext" 5 | rescue LoadError 6 | require "rocketamf_ext" 7 | end 8 | 9 | module RocketAMF 10 | # This module holds all the modules/classes that implement AMF's functionality 11 | # in C 12 | module Ext 13 | $DEBUG and warn "Using C library for RocketAMF." 14 | end 15 | 16 | #:stopdoc: 17 | # Import serializer/deserializer 18 | Deserializer = RocketAMF::Ext::Deserializer 19 | Serializer = RocketAMF::Ext::Serializer 20 | 21 | # Modify envelope so it can serialize/deserialize 22 | class Envelope 23 | remove_method :populate_from_stream 24 | remove_method :serialize 25 | include RocketAMF::Ext::Envelope 26 | end 27 | #:startdoc: 28 | end -------------------------------------------------------------------------------- /ext/rocketamf_ext/serializer.h: -------------------------------------------------------------------------------- 1 | #include 2 | #ifdef HAVE_RB_STR_ENCODE 3 | #include 4 | #include 5 | #else 6 | #include 7 | #endif 8 | 9 | typedef struct { 10 | int version; 11 | VALUE class_mapper; 12 | VALUE stream; 13 | long depth; 14 | st_table* str_cache; 15 | long str_index; 16 | st_table* trait_cache; 17 | long trait_index; 18 | st_table* obj_cache; 19 | long obj_index; 20 | } AMF_SERIALIZER; 21 | 22 | void ser_write_byte(AMF_SERIALIZER *ser, char byte); 23 | void ser_write_int(AMF_SERIALIZER *ser, int num); 24 | void ser_write_uint16(AMF_SERIALIZER *ser, long num); 25 | void ser_write_uint32(AMF_SERIALIZER *ser, long num); 26 | void ser_write_double(AMF_SERIALIZER *ser, double num); 27 | void ser_get_string(VALUE obj, VALUE encode, char** str, long* len); 28 | 29 | VALUE ser_serialize(VALUE self, VALUE ver, VALUE obj); -------------------------------------------------------------------------------- /RocketAMF.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | Gem::Specification.new do |s| 4 | s.name = 'RocketAMF' 5 | s.version = '1.0.0' 6 | s.platform = Gem::Platform::RUBY 7 | s.authors = ['Jacob Henry', 'Stephen Augenstein', "Joc O'Connor"] 8 | s.email = ['perl.programmer@gmail.com'] 9 | s.homepage = 'http://github.com/rubyamf/rocketamf' 10 | s.summary = 'Fast AMF serializer/deserializer with remoting request/response wrappers to simplify integration' 11 | 12 | s.files = Dir[*['README.rdoc', 'benchmark.rb', 'RocketAMF.gemspec', 'Rakefile', 'lib/**/*.rb', 'spec/**/*.{rb,bin,opts}', 'ext/**/*.{c,h,rb}']] 13 | s.test_files = Dir[*['spec/**/*_spec.rb']] 14 | s.extensions = Dir[*["ext/**/extconf.rb"]] 15 | s.require_paths = ["lib"] 16 | 17 | s.has_rdoc = true 18 | s.extra_rdoc_files = ['README.rdoc'] 19 | s.rdoc_options = ['--line-numbers', '--main', 'README.rdoc'] 20 | end -------------------------------------------------------------------------------- /ext/rocketamf_ext/deserializer.h: -------------------------------------------------------------------------------- 1 | #include 2 | #ifdef HAVE_RB_STR_ENCODE 3 | #include 4 | #endif 5 | 6 | typedef struct { 7 | int version; 8 | VALUE class_mapper; 9 | VALUE src; 10 | char* stream; 11 | unsigned long pos; 12 | unsigned long size; 13 | VALUE obj_cache; 14 | VALUE str_cache; 15 | VALUE trait_cache; 16 | } AMF_DESERIALIZER; 17 | 18 | char des_read_byte(AMF_DESERIALIZER *des); 19 | char des_read_ahead_byte(AMF_DESERIALIZER *des); 20 | int des_read_uint16(AMF_DESERIALIZER *des); 21 | unsigned int des_read_uint32(AMF_DESERIALIZER *des); 22 | double des_read_double(AMF_DESERIALIZER *des); 23 | int des_read_int(AMF_DESERIALIZER *des); 24 | VALUE des_read_string(AMF_DESERIALIZER *des, unsigned int len); 25 | VALUE des_read_sym(AMF_DESERIALIZER *des, unsigned int len); 26 | void des_set_src(AMF_DESERIALIZER *des, VALUE src); 27 | 28 | VALUE des_deserialize(VALUE self, VALUE ver, VALUE src); -------------------------------------------------------------------------------- /ext/rocketamf_ext/rocketamf_ext.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | VALUE mRocketAMF; 4 | VALUE mRocketAMFExt; 5 | VALUE cDeserializer; 6 | VALUE cSerializer; 7 | VALUE cStringIO; 8 | VALUE cDate; 9 | VALUE cDateTime; 10 | VALUE sym_class_name; 11 | VALUE sym_members; 12 | VALUE sym_externalizable; 13 | VALUE sym_dynamic; 14 | 15 | void Init_rocket_amf_deserializer(); 16 | void Init_rocket_amf_serializer(); 17 | void Init_rocket_amf_fast_class_mapping(); 18 | void Init_rocket_amf_remoting(); 19 | 20 | void Init_rocketamf_ext() { 21 | mRocketAMF = rb_define_module("RocketAMF"); 22 | mRocketAMFExt = rb_define_module_under(mRocketAMF, "Ext"); 23 | 24 | // Set up classes 25 | Init_rocket_amf_deserializer(); 26 | Init_rocket_amf_serializer(); 27 | Init_rocket_amf_fast_class_mapping(); 28 | Init_rocket_amf_remoting(); 29 | 30 | // Get refs to commonly used symbols and ids 31 | cStringIO = rb_const_get(rb_cObject, rb_intern("StringIO")); 32 | cDate = rb_const_get(rb_cObject, rb_intern("Date")); 33 | cDateTime = rb_const_get(rb_cObject, rb_intern("DateTime")); 34 | sym_class_name = ID2SYM(rb_intern("class_name")); 35 | sym_members = ID2SYM(rb_intern("members")); 36 | sym_externalizable = ID2SYM(rb_intern("externalizable")); 37 | sym_dynamic = ID2SYM(rb_intern("dynamic")); 38 | } -------------------------------------------------------------------------------- /spec/messages_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper.rb" 2 | 3 | describe RocketAMF::Values::AbstractMessage do 4 | before :each do 5 | @message = RocketAMF::Values::AbstractMessage.new 6 | end 7 | 8 | it "should generate conforming uuids" do 9 | @message.send(:rand_uuid).should =~ /[A-F0-9]{8}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{12}/i 10 | end 11 | 12 | it "should read externalized shortened BlazeDS messages" do 13 | env = create_envelope('blaze-response.bin') 14 | msg = env.messages[0].data 15 | msg.class.name.should == "RocketAMF::Values::AcknowledgeMessageExt" 16 | msg.clientId.should == "8814a067-fe0d-3a9c-a274-4aaed9bd7b0b" 17 | msg.body.should =~ /xmlsoap\.org/ 18 | end 19 | end 20 | 21 | describe RocketAMF::Values::ErrorMessage do 22 | before :each do 23 | @e = Exception.new('Error message') 24 | @e.set_backtrace(['Backtrace 1', 'Backtrace 2']) 25 | @message = RocketAMF::Values::ErrorMessage.new(nil, @e) 26 | end 27 | 28 | it "should serialize as a hash in AMF0" do 29 | response = RocketAMF::Envelope.new 30 | response.messages << RocketAMF::Message.new('1/onStatus', '', @message) 31 | response.serialize.should == request_fixture('amf0-error-response.bin') 32 | end 33 | 34 | it "should extract exception properties correctly" do 35 | @message.faultCode.should == 'Exception' 36 | @message.faultString.should == 'Error message' 37 | @message.faultDetail.should == "Backtrace 1\nBacktrace 2" 38 | end 39 | end -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rspec' 3 | require 'rspec/autorun' 4 | 5 | $:.unshift(File.dirname(__FILE__) + '/../lib') 6 | require 'rocketamf' 7 | require 'rocketamf/pure/io_helpers' # Just to make sure they get loaded 8 | 9 | def request_fixture(binary_path) 10 | data = File.open(File.dirname(__FILE__) + '/fixtures/request/' + binary_path, "rb").read 11 | data.force_encoding("ASCII-8BIT") if data.respond_to?(:force_encoding) 12 | data 13 | end 14 | 15 | def object_fixture(binary_path) 16 | data = File.open(File.dirname(__FILE__) + '/fixtures/objects/' + binary_path, "rb").read 17 | data.force_encoding("ASCII-8BIT") if data.respond_to?(:force_encoding) 18 | data 19 | end 20 | 21 | def create_envelope(binary_path) 22 | RocketAMF::Envelope.new.populate_from_stream(StringIO.new(request_fixture(binary_path))) 23 | end 24 | 25 | # Helper classes 26 | class RubyClass; attr_accessor :baz, :foo; end; 27 | class OtherClass; attr_accessor :bar, :foo; end; 28 | class ClassMappingTest 29 | attr_accessor :prop_a 30 | attr_accessor :prop_b 31 | end 32 | class ClassMappingTest2 < ClassMappingTest 33 | attr_accessor :prop_c 34 | end 35 | module ANamespace; class TestRubyClass; end; end 36 | class ExternalizableTest 37 | include RocketAMF::Pure::ReadIOHelpers 38 | include RocketAMF::Pure::WriteIOHelpers 39 | 40 | attr_accessor :one, :two 41 | 42 | def encode_amf serializer 43 | serializer.write_object(self, nil, {:class_name => 'ExternalizableTest', :dynamic => false, :externalizable => true, :members => []}) 44 | end 45 | 46 | def read_external des 47 | @one = read_double(des.source) 48 | @two = read_double(des.source) 49 | end 50 | 51 | def write_external ser 52 | ser.stream << pack_double(@one) 53 | ser.stream << pack_double(@two) 54 | end 55 | end -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rake' 3 | require 'rake/rdoctask' 4 | require 'rake/gempackagetask' 5 | require 'rspec/core/rake_task' 6 | require 'rake/extensiontask' 7 | 8 | desc 'Default: run the specs.' 9 | task :default => :spec 10 | 11 | # I don't want to depend on bundler, so we do it the bundler way without it 12 | gemspec_path = 'RocketAMF.gemspec' 13 | spec = begin 14 | eval(File.read(File.join(File.dirname(__FILE__), gemspec_path)), TOPLEVEL_BINDING, gemspec_path) 15 | rescue LoadError => e 16 | original_line = e.backtrace.find { |line| line.include?(gemspec_path) } 17 | msg = "There was a LoadError while evaluating #{gemspec_path}:\n #{e.message}" 18 | msg << " from\n #{original_line}" if original_line 19 | msg << "\n" 20 | puts msg 21 | exit 22 | end 23 | 24 | RSpec::Core::RakeTask.new do |t| 25 | end 26 | 27 | desc 'Generate documentation' 28 | Rake::RDocTask.new(:rdoc) do |rdoc| 29 | rdoc.rdoc_dir = 'rdoc' 30 | rdoc.title = spec.name 31 | rdoc.options += spec.rdoc_options 32 | rdoc.rdoc_files.include(*spec.extra_rdoc_files) 33 | rdoc.rdoc_files.include("lib") # Don't include ext folder because no one cares 34 | end 35 | 36 | Rake::GemPackageTask.new(spec) do |pkg| 37 | pkg.need_zip = false 38 | pkg.need_tar = false 39 | end 40 | 41 | Rake::ExtensionTask.new('rocketamf_ext', spec) do |ext| 42 | if RUBY_PLATFORM =~ /mswin|mingw/ then 43 | # No cross-compile on win, so compile extension to lib/1.[89] 44 | RUBY_VERSION =~ /(\d+\.\d+)/ 45 | ext.lib_dir = "lib/#{$1}" 46 | else 47 | ext.cross_compile = true 48 | ext.cross_platform = 'x86-mingw32' 49 | ext.cross_compiling do |gem_spec| 50 | gem_spec.post_install_message = "You installed the binary version of this gem!" 51 | end 52 | end 53 | #ext.config_options << '--enable-sort-props' 54 | end 55 | 56 | desc "Build gem packages" 57 | task :gems do 58 | sh "rake cross native gem RUBY_CC_VERSION=1.8.7:1.9.2" 59 | end -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | == DESCRIPTION: 2 | 3 | RocketAMF is a full featured AMF0/3 serializer and deserializer with support for 4 | bi-directional Flash to Ruby class mapping, custom serialization and mapping, 5 | remoting gateway helpers that follow AMF0/3 messaging specs, and a suite of specs 6 | to ensure adherence to the specification documents put out by Adobe. If the C 7 | components compile, then RocketAMF automatically takes advantage of them to 8 | provide a substantial performance benefit. In addition, RocketAMF is fully 9 | compatible with Ruby 1.9. 10 | 11 | == INSTALL: 12 | 13 | gem install RocketAMF 14 | 15 | == SIMPLE EXAMPLE: 16 | 17 | require 'rocketamf' 18 | 19 | hash = {:apple => "Apfel", :red => "Rot", :eyes => "Augen"} 20 | File.open("amf.dat", 'w') do |f| 21 | f.write RocketAMF.serialize(hash, 3) # Use AMF3 encoding to serialize 22 | end 23 | 24 | == LICENSE: 25 | 26 | (The MIT License) 27 | 28 | Copyright (c) 2011 Stephen Augenstein and Jacob Henry 29 | 30 | Permission is hereby granted, free of charge, to any person obtaining 31 | a copy of this software and associated documentation files (the 32 | 'Software'), to deal in the Software without restriction, including 33 | without limitation the rights to use, copy, modify, merge, publish, 34 | distribute, sublicense, and/or sell copies of the Software, and to 35 | permit persons to whom the Software is furnished to do so, subject to 36 | the following conditions: 37 | 38 | The above copyright notice and this permission notice shall be 39 | included in all copies or substantial portions of the Software. 40 | 41 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 42 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 43 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 44 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 45 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 46 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 47 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /lib/rocketamf/constants.rb: -------------------------------------------------------------------------------- 1 | module RocketAMF 2 | # AMF0 Type Markers 3 | AMF0_NUMBER_MARKER = 0x00 #"\000" 4 | AMF0_BOOLEAN_MARKER = 0x01 #"\001" 5 | AMF0_STRING_MARKER = 0x02 #"\002" 6 | AMF0_OBJECT_MARKER = 0x03 #"\003" 7 | AMF0_MOVIE_CLIP_MARKER = 0x04 #"\004" # Unused 8 | AMF0_NULL_MARKER = 0x05 #"\005" 9 | AMF0_UNDEFINED_MARKER = 0x06 #"\006" 10 | AMF0_REFERENCE_MARKER = 0x07 #"\a" 11 | AMF0_HASH_MARKER = 0x08 #"\b" 12 | AMF0_OBJECT_END_MARKER = 0x09 #"\t" 13 | AMF0_STRICT_ARRAY_MARKER = 0x0A #"\n" 14 | AMF0_DATE_MARKER = 0x0B #"\v" 15 | AMF0_LONG_STRING_MARKER = 0x0C #"\f" 16 | AMF0_UNSUPPORTED_MARKER = 0x0D #"\r" 17 | AMF0_RECORDSET_MARKER = 0x0E #"\016" # Unused 18 | AMF0_XML_MARKER = 0x0F #"\017" 19 | AMF0_TYPED_OBJECT_MARKER = 0x10 #"\020" 20 | AMF0_AMF3_MARKER = 0x11 #"\021" 21 | 22 | # AMF3 Type Markers 23 | AMF3_UNDEFINED_MARKER = 0x00 #"\000" 24 | AMF3_NULL_MARKER = 0x01 #"\001" 25 | AMF3_FALSE_MARKER = 0x02 #"\002" 26 | AMF3_TRUE_MARKER = 0x03 #"\003" 27 | AMF3_INTEGER_MARKER = 0x04 #"\004" 28 | AMF3_DOUBLE_MARKER = 0x05 #"\005" 29 | AMF3_STRING_MARKER = 0x06 #"\006" 30 | AMF3_XML_DOC_MARKER = 0x07 #"\a" 31 | AMF3_DATE_MARKER = 0x08 #"\b" 32 | AMF3_ARRAY_MARKER = 0x09 #"\t" 33 | AMF3_OBJECT_MARKER = 0x0A #"\n" 34 | AMF3_XML_MARKER = 0x0B #"\v" 35 | AMF3_BYTE_ARRAY_MARKER = 0x0C #"\f" 36 | AMF3_VECTOR_INT_MARKER = 0x0D #"\r" 37 | AMF3_VECTOR_UINT_MARKER = 0x0E #"\016" 38 | AMF3_VECTOR_DOUBLE_MARKER = 0x0F #"\017" 39 | AMF3_VECTOR_OBJECT_MARKER = 0x10 #"\020" 40 | AMF3_DICT_MARKER = 0x11 #"\021" 41 | 42 | # Other AMF3 Markers 43 | AMF3_EMPTY_STRING = 0x01 44 | AMF3_CLOSE_DYNAMIC_OBJECT = 0x01 45 | AMF3_CLOSE_DYNAMIC_ARRAY = 0x01 46 | 47 | # Other Constants 48 | MAX_INTEGER = 268435455 49 | MIN_INTEGER = -268435456 50 | end -------------------------------------------------------------------------------- /ext/rocketamf_ext/constants.h: -------------------------------------------------------------------------------- 1 | // AMF0 Type Markers 2 | #define AMF0_NUMBER_MARKER 0x00 3 | #define AMF0_BOOLEAN_MARKER 0x01 4 | #define AMF0_STRING_MARKER 0x02 5 | #define AMF0_OBJECT_MARKER 0x03 6 | #define AMF0_MOVIE_CLIP_MARKER 0x04 7 | #define AMF0_NULL_MARKER 0x05 8 | #define AMF0_UNDEFINED_MARKER 0x06 9 | #define AMF0_REFERENCE_MARKER 0x07 10 | #define AMF0_HASH_MARKER 0x08 11 | #define AMF0_OBJECT_END_MARKER 0x09 12 | #define AMF0_STRICT_ARRAY_MARKER 0x0A 13 | #define AMF0_DATE_MARKER 0x0B 14 | #define AMF0_LONG_STRING_MARKER 0x0C 15 | #define AMF0_UNSUPPORTED_MARKER 0x0D 16 | #define AMF0_RECORDSET_MARKER 0x0E 17 | #define AMF0_XML_MARKER 0x0F 18 | #define AMF0_TYPED_OBJECT_MARKER 0x10 19 | #define AMF0_AMF3_MARKER 0x11 20 | 21 | // AMF3 Type Markers 22 | #define AMF3_UNDEFINED_MARKER 0x00 23 | #define AMF3_NULL_MARKER 0x01 24 | #define AMF3_FALSE_MARKER 0x02 25 | #define AMF3_TRUE_MARKER 0x03 26 | #define AMF3_INTEGER_MARKER 0x04 27 | #define AMF3_DOUBLE_MARKER 0x05 28 | #define AMF3_STRING_MARKER 0x06 29 | #define AMF3_XML_DOC_MARKER 0x07 30 | #define AMF3_DATE_MARKER 0x08 31 | #define AMF3_ARRAY_MARKER 0x09 32 | #define AMF3_OBJECT_MARKER 0x0A 33 | #define AMF3_XML_MARKER 0x0B 34 | #define AMF3_BYTE_ARRAY_MARKER 0x0C 35 | #define AMF3_VECTOR_INT_MARKER 0x0D 36 | #define AMF3_VECTOR_UINT_MARKER 0x0E 37 | #define AMF3_VECTOR_DOUBLE_MARKER 0x0F 38 | #define AMF3_VECTOR_OBJECT_MARKER 0x10 39 | #define AMF3_DICT_MARKER 0x11 40 | 41 | // Other AMF3 Markers 42 | #define AMF3_EMPTY_STRING 0x01 43 | #define AMF3_DYNAMIC_OBJECT 0x0B 44 | #define AMF3_CLOSE_DYNAMIC_OBJECT 0x01 45 | #define AMF3_CLOSE_DYNAMIC_ARRAY 0x01 46 | 47 | // Other Constants 48 | #define MAX_INTEGER 268435455 49 | #define MIN_INTEGER -268435456 50 | #define INITIAL_STREAM_LENGTH 128 // Initial buffer length for serializer output 51 | #define MAX_STREAM_LENGTH 10*1024*1024 // Let's cap it at 10MB for now 52 | #define MAX_ARRAY_PREALLOC 100000 -------------------------------------------------------------------------------- /benchmark.rb: -------------------------------------------------------------------------------- 1 | $:.unshift(File.dirname(__FILE__) + '/ext') 2 | $:.unshift(File.dirname(__FILE__) + '/lib') 3 | require 'rubygems' 4 | require 'rocketamf' 5 | require 'rocketamf/pure/deserializer' # Only ext gets included by default if available 6 | require 'rocketamf/pure/serializer' 7 | 8 | OBJECT_COUNT = 100000 9 | TESTS = 5 10 | 11 | class TestClass 12 | attr_accessor :prop_a, :prop_b, :prop_c, :prop_d, :prop_e 13 | 14 | def populate some_arg=nil # Make sure class mapper doesn't think populate is a property 15 | @@count ||= 1 16 | @prop_a = "asdfasdf #{@@count}" 17 | @prop_b = "simple string" 18 | @prop_c = 3120094.03 19 | @prop_d = Time.now 20 | @prop_e = 3120094 21 | @@count += 1 22 | self 23 | end 24 | end 25 | 26 | objs = [] 27 | OBJECT_COUNT.times do 28 | objs << TestClass.new.populate 29 | end 30 | 31 | ["native", "pure"].each do |type| 32 | # Set up class mapper 33 | cm = if type == "pure" 34 | RocketAMF::ClassMapping 35 | else 36 | RocketAMF::Ext::FastClassMapping 37 | end 38 | cm.define do |m| 39 | m.map :as => 'TestClass', :ruby => 'TestClass' 40 | end 41 | 42 | [0, 3].each do |version| 43 | # 2**24 is larger than anyone is ever going to run this for 44 | min_serialize = 2**24 45 | min_deserialize = 2**24 46 | 47 | puts "Testing #{type} AMF#{version}:" 48 | TESTS.times do 49 | ser = if type == "pure" 50 | RocketAMF::Pure::Serializer.new(cm.new) 51 | else 52 | RocketAMF::Ext::Serializer.new(cm.new) 53 | end 54 | start_time = Time.now 55 | out = ser.serialize(version, objs) 56 | end_time = Time.now 57 | puts "\tserialize run: #{end_time-start_time}s" 58 | min_serialize = [end_time-start_time, min_serialize].min 59 | 60 | des = if type == "pure" 61 | RocketAMF::Pure::Deserializer.new(cm.new) 62 | else 63 | RocketAMF::Ext::Deserializer.new(cm.new) 64 | end 65 | start_time = Time.now 66 | temp = des.deserialize(version, out) 67 | end_time = Time.now 68 | puts "\tdeserialize run: #{end_time-start_time}s" 69 | min_deserialize = [end_time-start_time, min_deserialize].min 70 | end 71 | puts "\tminimum serialize time: #{min_serialize}s" 72 | puts "\tminimum deserialize time: #{min_deserialize}s" 73 | end 74 | end -------------------------------------------------------------------------------- /lib/rocketamf/pure/io_helpers.rb: -------------------------------------------------------------------------------- 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 -------------------------------------------------------------------------------- /spec/class_mapping_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper.rb" 2 | 3 | describe RocketAMF::ClassMapping do 4 | before :each do 5 | RocketAMF::ClassMapping.reset 6 | RocketAMF::ClassMapping.define do |m| 7 | m.map :as => 'ASClass', :ruby => 'ClassMappingTest' 8 | end 9 | @mapper = RocketAMF::ClassMapping.new 10 | end 11 | 12 | describe "class name mapping" do 13 | it "should allow resetting of mappings back to defaults" do 14 | @mapper.get_as_class_name('ClassMappingTest').should_not be_nil 15 | RocketAMF::ClassMapping.reset 16 | @mapper = RocketAMF::ClassMapping.new 17 | @mapper.get_as_class_name('ClassMappingTest').should be_nil 18 | @mapper.get_as_class_name('RocketAMF::Values::AcknowledgeMessage').should_not be_nil 19 | end 20 | 21 | it "should return AS class name for ruby objects" do 22 | @mapper.get_as_class_name(ClassMappingTest.new).should == 'ASClass' 23 | @mapper.get_as_class_name('ClassMappingTest').should == 'ASClass' 24 | @mapper.get_as_class_name(RocketAMF::Values::TypedHash.new('ClassMappingTest')).should == 'ASClass' 25 | @mapper.get_as_class_name('BadClass').should be_nil 26 | end 27 | 28 | it "should instantiate a ruby class" do 29 | @mapper.get_ruby_obj('ASClass').should be_a(ClassMappingTest) 30 | end 31 | 32 | it "should properly instantiate namespaced classes" do 33 | RocketAMF::ClassMapping.mappings.map :as => 'ASClass', :ruby => 'ANamespace::TestRubyClass' 34 | @mapper = RocketAMF::ClassMapping.new 35 | @mapper.get_ruby_obj('ASClass').should be_a(ANamespace::TestRubyClass) 36 | end 37 | 38 | it "should return a hash with original type if not mapped" do 39 | obj = @mapper.get_ruby_obj('UnmappedClass') 40 | obj.should be_a(RocketAMF::Values::TypedHash) 41 | obj.type.should == 'UnmappedClass' 42 | end 43 | 44 | it "should map special classes from AS by default" do 45 | as_classes = [ 46 | 'flex.messaging.messages.AcknowledgeMessage', 47 | 'flex.messaging.messages.CommandMessage', 48 | 'flex.messaging.messages.RemotingMessage' 49 | ] 50 | 51 | as_classes.each do |as_class| 52 | @mapper.get_ruby_obj(as_class).should_not be_a(RocketAMF::Values::TypedHash) 53 | end 54 | end 55 | 56 | it "should map special classes from ruby by default" do 57 | ruby_classes = [ 58 | 'RocketAMF::Values::AcknowledgeMessage', 59 | 'RocketAMF::Values::ErrorMessage' 60 | ] 61 | 62 | ruby_classes.each do |obj| 63 | @mapper.get_as_class_name(obj).should_not be_nil 64 | end 65 | end 66 | 67 | it "should allow config modification" do 68 | RocketAMF::ClassMapping.mappings.map :as => 'SecondClass', :ruby => 'ClassMappingTest' 69 | @mapper = RocketAMF::ClassMapping.new 70 | @mapper.get_as_class_name(ClassMappingTest.new).should == 'SecondClass' 71 | end 72 | end 73 | 74 | describe "ruby object populator" do 75 | it "should populate a ruby class" do 76 | obj = @mapper.populate_ruby_obj ClassMappingTest.new, {:prop_a => 'Data'} 77 | obj.prop_a.should == 'Data' 78 | end 79 | 80 | it "should populate a typed hash" do 81 | obj = @mapper.populate_ruby_obj RocketAMF::Values::TypedHash.new('UnmappedClass'), {:prop_a => 'Data'} 82 | obj[:prop_a].should == 'Data' 83 | end 84 | end 85 | 86 | describe "property extractor" do 87 | it "should extract hash properties" do 88 | hash = {:a => 'test1', 'b' => 'test2'} 89 | props = @mapper.props_for_serialization(hash) 90 | props.should == {'a' => 'test1', 'b' => 'test2'} 91 | end 92 | 93 | it "should extract object properties" do 94 | obj = ClassMappingTest.new 95 | obj.prop_a = 'Test A' 96 | 97 | hash = @mapper.props_for_serialization obj 98 | hash.should == {'prop_a' => 'Test A', 'prop_b' => nil} 99 | end 100 | 101 | it "should extract inherited object properties" do 102 | obj = ClassMappingTest2.new 103 | obj.prop_a = 'Test A' 104 | obj.prop_c = 'Test C' 105 | 106 | hash = @mapper.props_for_serialization obj 107 | hash.should == {'prop_a' => 'Test A', 'prop_b' => nil, 'prop_c' => 'Test C'} 108 | end 109 | end 110 | end -------------------------------------------------------------------------------- /lib/rocketamf/pure/remoting.rb: -------------------------------------------------------------------------------- 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 -------------------------------------------------------------------------------- /spec/fast_class_mapping_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper.rb" 2 | 3 | describe RocketAMF::Ext::FastClassMapping do 4 | before :each do 5 | RocketAMF::Ext::FastClassMapping.reset 6 | RocketAMF::Ext::FastClassMapping.define do |m| 7 | m.map :as => 'ASClass', :ruby => 'ClassMappingTest' 8 | end 9 | @mapper = RocketAMF::Ext::FastClassMapping.new 10 | end 11 | 12 | describe "class name mapping" do 13 | it "should allow resetting of mappings back to defaults" do 14 | @mapper.get_as_class_name('ClassMappingTest').should_not be_nil 15 | RocketAMF::Ext::FastClassMapping.reset 16 | @mapper = RocketAMF::Ext::FastClassMapping.new 17 | @mapper.get_as_class_name('ClassMappingTest').should be_nil 18 | @mapper.get_as_class_name('RocketAMF::Values::AcknowledgeMessage').should_not be_nil 19 | end 20 | 21 | it "should return AS class name for ruby objects" do 22 | @mapper.get_as_class_name(ClassMappingTest.new).should == 'ASClass' 23 | @mapper.get_as_class_name('ClassMappingTest').should == 'ASClass' 24 | @mapper.get_as_class_name(RocketAMF::Values::TypedHash.new('ClassMappingTest')).should == 'ASClass' 25 | @mapper.get_as_class_name('BadClass').should be_nil 26 | end 27 | 28 | it "should instantiate a ruby class" do 29 | @mapper.get_ruby_obj('ASClass').should be_a(ClassMappingTest) 30 | end 31 | 32 | it "should properly instantiate namespaced classes" do 33 | RocketAMF::Ext::FastClassMapping.mappings.map :as => 'ASClass', :ruby => 'ANamespace::TestRubyClass' 34 | @mapper = RocketAMF::Ext::FastClassMapping.new 35 | @mapper.get_ruby_obj('ASClass').should be_a(ANamespace::TestRubyClass) 36 | end 37 | 38 | it "should return a hash with original type if not mapped" do 39 | obj = @mapper.get_ruby_obj('UnmappedClass') 40 | obj.should be_a(RocketAMF::Values::TypedHash) 41 | obj.type.should == 'UnmappedClass' 42 | end 43 | 44 | it "should map special classes from AS by default" do 45 | as_classes = [ 46 | 'flex.messaging.messages.AcknowledgeMessage', 47 | 'flex.messaging.messages.CommandMessage', 48 | 'flex.messaging.messages.RemotingMessage' 49 | ] 50 | 51 | as_classes.each do |as_class| 52 | @mapper.get_ruby_obj(as_class).should_not be_a(RocketAMF::Values::TypedHash) 53 | end 54 | end 55 | 56 | it "should map special classes from ruby by default" do 57 | ruby_classes = [ 58 | 'RocketAMF::Values::AcknowledgeMessage', 59 | 'RocketAMF::Values::ErrorMessage' 60 | ] 61 | 62 | ruby_classes.each do |obj| 63 | @mapper.get_as_class_name(obj).should_not be_nil 64 | end 65 | end 66 | 67 | it "should allow config modification" do 68 | RocketAMF::Ext::FastClassMapping.mappings.map :as => 'SecondClass', :ruby => 'ClassMappingTest' 69 | @mapper = RocketAMF::Ext::FastClassMapping.new 70 | @mapper.get_as_class_name(ClassMappingTest.new).should == 'SecondClass' 71 | end 72 | end 73 | 74 | describe "ruby object populator" do 75 | it "should populate a ruby class" do 76 | obj = @mapper.populate_ruby_obj ClassMappingTest.new, {:prop_a => 'Data'} 77 | obj.prop_a.should == 'Data' 78 | end 79 | 80 | it "should populate a typed hash" do 81 | obj = @mapper.populate_ruby_obj RocketAMF::Values::TypedHash.new('UnmappedClass'), {'prop_a' => 'Data'} 82 | obj['prop_a'].should == 'Data' 83 | end 84 | end 85 | 86 | describe "property extractor" do 87 | # Use symbol keys for properties in Ruby >1.9 88 | def prop_hash hash 89 | out = {} 90 | if RUBY_VERSION =~ /^1\.8/ 91 | hash.each {|k,v| out[k.to_s] = v} 92 | else 93 | hash.each {|k,v| out[k.to_sym] = v} 94 | end 95 | out 96 | end 97 | 98 | it "should return hash without modification" do 99 | hash = {:a => 'test1', 'b' => 'test2'} 100 | props = @mapper.props_for_serialization(hash) 101 | props.should === hash 102 | end 103 | 104 | it "should extract object properties" do 105 | obj = ClassMappingTest.new 106 | obj.prop_a = 'Test A' 107 | 108 | hash = @mapper.props_for_serialization obj 109 | hash.should == prop_hash({'prop_a' => 'Test A', 'prop_b' => nil}) 110 | end 111 | 112 | it "should extract inherited object properties" do 113 | obj = ClassMappingTest2.new 114 | obj.prop_a = 'Test A' 115 | obj.prop_c = 'Test C' 116 | 117 | hash = @mapper.props_for_serialization obj 118 | hash.should == prop_hash({'prop_a' => 'Test A', 'prop_b' => nil, 'prop_c' => 'Test C'}) 119 | end 120 | 121 | it "should cache property lookups by instance" do 122 | class ClassMappingTest3; attr_accessor :prop_a; end; 123 | 124 | # Cache properties 125 | obj = ClassMappingTest3.new 126 | hash = @mapper.props_for_serialization obj 127 | 128 | # Add a method to ClassMappingTest3 129 | class ClassMappingTest3; attr_accessor :prop_b; end; 130 | 131 | # Test property list does not have new property 132 | obj = ClassMappingTest3.new 133 | obj.prop_a = 'Test A' 134 | obj.prop_b = 'Test B' 135 | hash = @mapper.props_for_serialization obj 136 | hash.should == prop_hash({'prop_a' => 'Test A'}) 137 | 138 | # Test that new class mapper *does* have new property (cache per instance) 139 | @mapper = RocketAMF::Ext::FastClassMapping.new 140 | hash = @mapper.props_for_serialization obj 141 | hash.should == prop_hash({'prop_a' => 'Test A', 'prop_b' => 'Test B'}) 142 | end 143 | end 144 | end -------------------------------------------------------------------------------- /lib/rocketamf/values/messages.rb: -------------------------------------------------------------------------------- 1 | module RocketAMF 2 | module Values #:nodoc: 3 | # Base class for all special AS3 response messages. Maps to 4 | # flex.messaging.messages.AbstractMessage. 5 | class AbstractMessage 6 | EXTERNALIZABLE_FIELDS = [ 7 | %w[ body clientId destination headers messageId timestamp timeToLive ], 8 | %w[ clientIdBytes messageIdBytes ] 9 | ] 10 | attr_accessor :clientId 11 | attr_accessor :destination 12 | attr_accessor :messageId 13 | attr_accessor :timestamp 14 | attr_accessor :timeToLive 15 | attr_accessor :headers 16 | attr_accessor :body 17 | 18 | def clientIdBytes= bytes 19 | @clientId = pretty_uuid(bytes) unless bytes.nil? 20 | end 21 | 22 | def messageIdBytes= bytes 23 | @messageId = pretty_uuid(bytes) unless bytes.nil? 24 | end 25 | 26 | def read_external des 27 | read_external_fields des, EXTERNALIZABLE_FIELDS 28 | end 29 | 30 | private 31 | def rand_uuid 32 | [8,4,4,4,12].map {|n| rand_hex_3(n)}.join('-').to_s 33 | end 34 | 35 | def rand_hex_3(l) 36 | "%0#{l}x" % rand(1 << l*4) 37 | end 38 | 39 | def pretty_uuid bytes 40 | "%08x-%04x-%04x-%04x-%08x%04x" % bytes.string.unpack("NnnnNn") 41 | end 42 | 43 | def read_external_fields des, fields 44 | # Read flags 45 | flags = [] 46 | loop do 47 | flags << des.source.read(1).unpack('C').first 48 | break if flags.last < 128 49 | end 50 | 51 | # Read fields and any remaining unmapped fields in a byte-set 52 | fields.each_with_index do |list, i| 53 | break if flags[i].nil? 54 | 55 | list.each_with_index do |name, j| 56 | if flags[i] & 2**j != 0 57 | send("#{name}=", des.read_object) 58 | end 59 | end 60 | 61 | # Read remaining flags even though we don't recognize them 62 | # Zero out high bit, as it's the has-next-field marker 63 | f = (flags[i] & ~128) >> list.length 64 | while f > 0 65 | des.read_object if (f & 1) != 0 66 | f >>= 1 67 | end 68 | end 69 | end 70 | end 71 | 72 | # Maps to flex.messaging.messages.RemotingMessage 73 | class RemotingMessage < AbstractMessage 74 | # The name of the service to be called including package name 75 | attr_accessor :source 76 | 77 | # The name of the method to be called 78 | attr_accessor :operation 79 | 80 | def initialize 81 | @clientId = nil 82 | @destination = nil 83 | @messageId = rand_uuid 84 | @timestamp = Time.new.to_i*100 85 | @timeToLive = 0 86 | @headers = {} 87 | @body = nil 88 | end 89 | end 90 | 91 | # Maps to flex.messaging.messages.AsyncMessage 92 | class AsyncMessage < AbstractMessage 93 | EXTERNALIZABLE_FIELDS = [ 94 | %w[ correlationId correlationIdBytes] 95 | ] 96 | attr_accessor :correlationId 97 | 98 | def correlationIdBytes= bytes 99 | @correlationId = pretty_uuid(bytes) unless bytes.nil? 100 | end 101 | 102 | def read_external des 103 | super des 104 | read_external_fields des, EXTERNALIZABLE_FIELDS 105 | end 106 | end 107 | 108 | class AsyncMessageExt < AsyncMessage #:nodoc: 109 | end 110 | 111 | # Maps to flex.messaging.messages.CommandMessage 112 | class CommandMessage < AsyncMessage 113 | SUBSCRIBE_OPERATION = 0 114 | UNSUSBSCRIBE_OPERATION = 1 115 | POLL_OPERATION = 2 116 | CLIENT_SYNC_OPERATION = 4 117 | CLIENT_PING_OPERATION = 5 118 | CLUSTER_REQUEST_OPERATION = 7 119 | LOGIN_OPERATION = 8 120 | LOGOUT_OPERATION = 9 121 | SESSION_INVALIDATE_OPERATION = 10 122 | MULTI_SUBSCRIBE_OPERATION = 11 123 | DISCONNECT_OPERATION = 12 124 | UNKNOWN_OPERATION = 10000 125 | 126 | EXTERNALIZABLE_FIELDS = [ 127 | %w[ operation ] 128 | ] 129 | attr_accessor :operation 130 | 131 | def initialize 132 | @operation = UNKNOWN_OPERATION 133 | end 134 | 135 | def read_external des 136 | super des 137 | read_external_fields des, EXTERNALIZABLE_FIELDS 138 | end 139 | end 140 | 141 | class CommandMessageExt < CommandMessage #:nodoc: 142 | end 143 | 144 | # Maps to flex.messaging.messages.AcknowledgeMessage 145 | class AcknowledgeMessage < AsyncMessage 146 | EXTERNALIZABLE_FIELDS = [[]] 147 | 148 | def initialize message=nil 149 | @clientId = rand_uuid 150 | @destination = nil 151 | @messageId = rand_uuid 152 | @timestamp = Time.new.to_i*100 153 | @timeToLive = 0 154 | @headers = {} 155 | @body = nil 156 | 157 | if message.is_a?(AbstractMessage) 158 | @correlationId = message.messageId 159 | end 160 | end 161 | 162 | def read_external des 163 | super des 164 | read_external_fields des, EXTERNALIZABLE_FIELDS 165 | end 166 | end 167 | 168 | class AcknowledgeMessageExt < AcknowledgeMessage #:nodoc: 169 | end 170 | 171 | # Maps to flex.messaging.messages.ErrorMessage in AMF3 mode 172 | class ErrorMessage < AcknowledgeMessage 173 | # Extended data that will facilitate custom error processing on the client 174 | attr_accessor :extendedData 175 | 176 | # The fault code for the error, which defaults to the class name of the 177 | # causing exception 178 | attr_accessor :faultCode 179 | 180 | # Detailed description of what caused the error 181 | attr_accessor :faultDetail 182 | 183 | # A simple description of the error 184 | attr_accessor :faultString 185 | 186 | # Optional "root cause" of the error 187 | attr_accessor :rootCause 188 | 189 | def initialize message=nil, exception=nil 190 | super message 191 | 192 | unless exception.nil? 193 | @e = exception 194 | @faultCode = @e.class.name 195 | @faultDetail = @e.backtrace.join("\n") 196 | @faultString = @e.message 197 | end 198 | end 199 | 200 | def encode_amf serializer 201 | if serializer.version == 0 202 | data = { 203 | :faultCode => @faultCode, 204 | :faultDetail => @faultDetail, 205 | :faultString => @faultString 206 | } 207 | serializer.write_object(data) 208 | else 209 | serializer.write_object(self) 210 | end 211 | end 212 | end 213 | end 214 | end -------------------------------------------------------------------------------- /ext/rocketamf_ext/remoting.c: -------------------------------------------------------------------------------- 1 | #include "deserializer.h" 2 | #include "serializer.h" 3 | #include "constants.h" 4 | 5 | extern VALUE mRocketAMF; 6 | extern VALUE mRocketAMFExt; 7 | extern VALUE cDeserializer; 8 | extern VALUE cSerializer; 9 | VALUE cRocketAMFHeader; 10 | VALUE cRocketAMFMessage; 11 | VALUE cRocketAMFAbstractMessage; 12 | ID id_amf_version; 13 | ID id_headers; 14 | ID id_messages; 15 | ID id_data; 16 | 17 | /* 18 | * call-seq: 19 | * env.populate_from_stream(stream, class_mapper=nil) 20 | * 21 | * Included into RocketAMF::Envelope, this method handles deserializing an AMF 22 | * request/response into the envelope 23 | */ 24 | static VALUE env_populate_from_stream(int argc, VALUE *argv, VALUE self) { 25 | static VALUE cClassMapper = 0; 26 | if(cClassMapper == 0) cClassMapper = rb_const_get(mRocketAMF, rb_intern("ClassMapper")); 27 | 28 | // Parse args 29 | VALUE src; 30 | VALUE class_mapper; 31 | rb_scan_args(argc, argv, "11", &src, &class_mapper); 32 | if(class_mapper == Qnil) class_mapper = rb_class_new_instance(0, NULL, cClassMapper); 33 | 34 | // Create AMF0 deserializer 35 | VALUE args[3]; 36 | args[0] = class_mapper; 37 | VALUE des_rb = rb_class_new_instance(1, args, cDeserializer); 38 | AMF_DESERIALIZER *des; 39 | Data_Get_Struct(des_rb, AMF_DESERIALIZER, des); 40 | des_set_src(des, src); 41 | 42 | // Read amf version 43 | int amf_ver = des_read_uint16(des); 44 | 45 | // Read headers 46 | VALUE headers = rb_hash_new(); 47 | int header_cnt = des_read_uint16(des); 48 | int i; 49 | for(i = 0; i < header_cnt; i++) { 50 | VALUE name = des_read_string(des, des_read_uint16(des)); 51 | VALUE must_understand = des_read_byte(des) != 0 ? Qtrue : Qfalse; 52 | des_read_uint32(des); // Length is ignored 53 | VALUE data = des_deserialize(des_rb, INT2FIX(0), Qnil); 54 | 55 | args[0] = name; 56 | args[1] = must_understand; 57 | args[2] = data; 58 | rb_hash_aset(headers, name, rb_class_new_instance(3, args, cRocketAMFHeader)); 59 | } 60 | 61 | // Read messages 62 | VALUE messages = rb_ary_new(); 63 | int message_cnt = des_read_uint16(des); 64 | for(i = 0; i < message_cnt; i++) { 65 | VALUE target_uri = des_read_string(des, des_read_uint16(des)); 66 | VALUE response_uri = des_read_string(des, des_read_uint16(des)); 67 | des_read_uint32(des); // Length is ignored 68 | VALUE data = des_deserialize(des_rb, INT2FIX(0), Qnil); 69 | 70 | // If they're using the flex remoting APIs, remove array wrapper 71 | if(TYPE(data) == T_ARRAY && RARRAY_LEN(data) == 1 && rb_obj_is_kind_of(RARRAY_PTR(data)[0], cRocketAMFAbstractMessage) == Qtrue) { 72 | data = RARRAY_PTR(data)[0]; 73 | } 74 | 75 | args[0] = target_uri; 76 | args[1] = response_uri; 77 | args[2] = data; 78 | rb_ary_push(messages, rb_class_new_instance(3, args, cRocketAMFMessage)); 79 | } 80 | 81 | // Populate remoting object 82 | rb_ivar_set(self, id_amf_version, INT2FIX(amf_ver)); 83 | rb_ivar_set(self, id_headers, headers); 84 | rb_ivar_set(self, id_messages, messages); 85 | 86 | return self; 87 | } 88 | 89 | /* 90 | * call-seq: 91 | * env.serialize(class_mapper=nil) 92 | * 93 | * Included into RocketAMF::Envelope, this method handles serializing an AMF 94 | * request/response into a string 95 | */ 96 | static VALUE env_serialize(int argc, VALUE *argv, VALUE self) { 97 | static VALUE cClassMapper = 0; 98 | if(cClassMapper == 0) cClassMapper = rb_const_get(mRocketAMF, rb_intern("ClassMapper")); 99 | 100 | // Parse args 101 | VALUE class_mapper; 102 | rb_scan_args(argc, argv, "01", &class_mapper); 103 | if(class_mapper == Qnil) class_mapper = rb_class_new_instance(0, NULL, cClassMapper); 104 | 105 | // Get instance variables 106 | long amf_ver = FIX2LONG(rb_ivar_get(self, id_amf_version)); 107 | VALUE headers = rb_funcall(rb_ivar_get(self, id_headers), rb_intern("values"), 0); // Get array of header values 108 | VALUE messages = rb_ivar_get(self, id_messages); 109 | 110 | // Create AMF0 serializer 111 | VALUE args[1] = {class_mapper}; 112 | VALUE ser_rb = rb_class_new_instance(1, args, cSerializer); 113 | AMF_SERIALIZER *ser; 114 | Data_Get_Struct(ser_rb, AMF_SERIALIZER, ser); 115 | 116 | // Write version 117 | ser_write_uint16(ser, amf_ver); 118 | 119 | // Write headers 120 | long header_cnt = RARRAY_LEN(headers); 121 | ser_write_uint16(ser, header_cnt); 122 | int i; 123 | char *str; 124 | long str_len; 125 | for(i = 0; i < header_cnt; i++) { 126 | VALUE header = RARRAY_PTR(headers)[i]; 127 | 128 | // Write header name 129 | ser_get_string(rb_funcall(header, rb_intern("name"), 0), Qtrue, &str, &str_len); 130 | ser_write_uint16(ser, str_len); 131 | rb_str_buf_cat(ser->stream, str, str_len); 132 | 133 | // Write understand flag 134 | ser_write_byte(ser, rb_funcall(header, rb_intern("must_understand"), 0) == Qtrue ? 1 : 0); 135 | 136 | // Serialize data 137 | ser_write_uint32(ser, -1); // length of data - -1 if you don't know 138 | ser_serialize(ser_rb, INT2FIX(0), rb_funcall(header, id_data, 0)); 139 | } 140 | 141 | // Write messages 142 | long message_cnt = RARRAY_LEN(messages); 143 | ser_write_uint16(ser, message_cnt); 144 | for(i = 0; i < message_cnt; i++) { 145 | VALUE message = RARRAY_PTR(messages)[i]; 146 | 147 | // Write target_uri 148 | ser_get_string(rb_funcall(message, rb_intern("target_uri"), 0), Qtrue, &str, &str_len); 149 | ser_write_uint16(ser, str_len); 150 | rb_str_buf_cat(ser->stream, str, str_len); 151 | 152 | // Write response_uri 153 | ser_get_string(rb_funcall(message, rb_intern("response_uri"), 0), Qtrue, &str, &str_len); 154 | ser_write_uint16(ser, str_len); 155 | rb_str_buf_cat(ser->stream, str, str_len); 156 | 157 | // Serialize data 158 | ser_write_uint32(ser, -1); // length of data - -1 if you don't know 159 | if(amf_ver == 3) { 160 | ser_write_byte(ser, AMF0_AMF3_MARKER); 161 | ser_serialize(ser_rb, INT2FIX(3), rb_funcall(message, id_data, 0)); 162 | } else { 163 | ser_serialize(ser_rb, INT2FIX(0), rb_funcall(message, id_data, 0)); 164 | } 165 | } 166 | 167 | return ser->stream; 168 | } 169 | 170 | 171 | void Init_rocket_amf_remoting() { 172 | VALUE mEnvelope = rb_define_module_under(mRocketAMFExt, "Envelope"); 173 | rb_define_method(mEnvelope, "populate_from_stream", env_populate_from_stream, -1); 174 | rb_define_method(mEnvelope, "serialize", env_serialize, -1); 175 | 176 | // Get refs to commonly used symbols and ids 177 | id_amf_version = rb_intern("@amf_version"); 178 | id_headers = rb_intern("@headers"); 179 | id_messages = rb_intern("@messages"); 180 | id_data = rb_intern("data"); 181 | cRocketAMFHeader = rb_const_get(mRocketAMF, rb_intern("Header")); 182 | cRocketAMFMessage = rb_const_get(mRocketAMF, rb_intern("Message")); 183 | cRocketAMFAbstractMessage = rb_const_get(rb_const_get(mRocketAMF, rb_intern("Values")), rb_intern("AbstractMessage")); 184 | } -------------------------------------------------------------------------------- /spec/remoting_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper.rb" 2 | 3 | describe RocketAMF::Envelope do 4 | describe 'deserializer' do 5 | it "should handle remoting message from remote object" do 6 | req = create_envelope("remotingMessage.bin") 7 | 8 | req.headers.length.should == 0 9 | req.messages.length.should == 1 10 | message = req.messages[0].data 11 | message.should be_a(RocketAMF::Values::RemotingMessage) 12 | message.messageId.should == "FE4AF2BC-DD3C-5470-05D8-9971D51FF89D" 13 | message.body.should == [true] 14 | end 15 | 16 | it "should handle command message from remote object" do 17 | req = create_envelope("commandMessage.bin") 18 | 19 | req.headers.length.should == 0 20 | req.messages.length.should == 1 21 | message = req.messages[0].data 22 | message.should be_a(RocketAMF::Values::CommandMessage) 23 | message.messageId.should == "7B0ACE15-8D57-6AE5-B9D4-99C2D32C8246" 24 | message.body.should == {} 25 | end 26 | end 27 | 28 | describe 'request builder' do 29 | it "should create simple call" do 30 | req = RocketAMF::Envelope.new 31 | req.call('TestController.test', 'first_arg', 'second_arg') 32 | 33 | expected = request_fixture('simple-request.bin') 34 | req.serialize.should == expected 35 | end 36 | 37 | it "should allow multiple simple calls" do 38 | req = RocketAMF::Envelope.new 39 | req.call('TestController.test', 'first_arg', 'second_arg') 40 | req.call('TestController.test2', 'first_arg', 'second_arg') 41 | 42 | expected = request_fixture('multiple-simple-request.bin') 43 | req.serialize.should == expected 44 | end 45 | 46 | it "should create flex remoting call" do 47 | req = RocketAMF::Envelope.new :amf_version => 3 48 | req.call_flex('TestController.test', 'first_arg', 'second_arg') 49 | req.messages[0].data.timestamp = 0 50 | req.messages[0].data.messageId = "9D108E33-B591-BE79-210D-F1A72D06B578" 51 | 52 | expected = request_fixture('flex-request.bin') 53 | req.serialize.should == expected 54 | end 55 | 56 | it "should require AMF version 3 for remoting calls" do 57 | req = RocketAMF::Envelope.new :amf_version => 0 58 | lambda { 59 | req.call_flex('TestController.test') 60 | }.should raise_error("Cannot use flex remoting calls with AMF0") 61 | end 62 | 63 | it "should require all calls be the same type" do 64 | req = RocketAMF::Envelope.new :amf_version => 0 65 | lambda { 66 | req.call('TestController.test') 67 | req.call_flex('TestController.test') 68 | }.should raise_error("Cannot use different call types") 69 | end 70 | end 71 | 72 | describe 'serializer' do 73 | it "should serialize response when converted to string" do 74 | res = RocketAMF::Envelope.new 75 | res.should_receive(:serialize).and_return('serialized') 76 | res.to_s.should == 'serialized' 77 | end 78 | 79 | it "should serialize a simple call" do 80 | res = RocketAMF::Envelope.new :amf_version => 3 81 | res.messages << RocketAMF::Message.new('/1/onResult', '', 'hello') 82 | 83 | expected = request_fixture('simple-response.bin') 84 | res.serialize.should == expected 85 | end 86 | 87 | it "should serialize a AcknowledgeMessage response" do 88 | ak = RocketAMF::Values::AcknowledgeMessage.new 89 | ak.clientId = "7B0ACE15-8D57-6AE5-B9D4-99C2D32C8246" 90 | ak.messageId = "7B0ACE15-8D57-6AE5-B9D4-99C2D32C8246" 91 | ak.timestamp = 0 92 | res = RocketAMF::Envelope.new :amf_version => 3 93 | res.messages << RocketAMF::Message.new('/1/onResult', '', ak) 94 | 95 | expected = request_fixture('acknowledge-response.bin') 96 | res.serialize.should == expected 97 | end 98 | end 99 | 100 | describe 'message handler' do 101 | it "should respond to ping command" do 102 | res = RocketAMF::Envelope.new 103 | req = create_envelope('commandMessage.bin') 104 | res.each_method_call req do |method, args| 105 | nil 106 | end 107 | 108 | res.messages.length.should == 1 109 | res.messages[0].data.should be_a(RocketAMF::Values::AcknowledgeMessage) 110 | end 111 | 112 | it "should fail on unsupported command" do 113 | res = RocketAMF::Envelope.new 114 | req = create_envelope('unsupportedCommandMessage.bin') 115 | res.each_method_call req do |method, args| 116 | nil 117 | end 118 | 119 | res.messages.length.should == 1 120 | res.messages[0].data.should be_a(RocketAMF::Values::ErrorMessage) 121 | res.messages[0].data.faultString.should == "CommandMessage 10000 not implemented" 122 | end 123 | 124 | it "should handle RemotingMessages properly" do 125 | res = RocketAMF::Envelope.new 126 | req = create_envelope('remotingMessage.bin') 127 | res.each_method_call req do |method, args| 128 | method.should == 'WritesController.save' 129 | args.should == [true] 130 | true 131 | end 132 | 133 | res.messages.length.should == 1 134 | res.messages[0].data.should be_a(RocketAMF::Values::AcknowledgeMessage) 135 | res.messages[0].data.body.should == true 136 | end 137 | 138 | it "should catch exceptions properly" do 139 | res = RocketAMF::Envelope.new 140 | req = create_envelope('remotingMessage.bin') 141 | res.each_method_call req do |method, args| 142 | raise 'Error in call' 143 | end 144 | 145 | res.messages.length.should == 1 146 | res.messages[0].data.should be_a(RocketAMF::Values::ErrorMessage) 147 | res.messages[0].target_uri.should =~ /onStatus$/ 148 | end 149 | 150 | it "should not crash if source missing on RemotingMessage" do 151 | res = RocketAMF::Envelope.new 152 | req = create_envelope('remotingMessage.bin') 153 | req.messages[0].data.instance_variable_set("@source", nil) 154 | lambda { 155 | res.each_method_call req do |method,args| 156 | true 157 | end 158 | }.should_not raise_error 159 | end 160 | end 161 | 162 | describe 'response parser' do 163 | it "should return the result of a simple response" do 164 | req = RocketAMF::Envelope.new 165 | req.call('TestController.test', 'first_arg', 'second_arg') 166 | res = RocketAMF::Envelope.new 167 | res.each_method_call req do |method, args| 168 | ['a', 'b'] 169 | end 170 | 171 | res.result.should == ['a', 'b'] 172 | end 173 | 174 | it "should return the results of multiple simple response in a single request" do 175 | req = RocketAMF::Envelope.new 176 | req.call('TestController.test', 'first_arg', 'second_arg') 177 | req.call('TestController.test2', 'first_arg', 'second_arg') 178 | res = RocketAMF::Envelope.new 179 | res.each_method_call req do |method, args| 180 | ['a', 'b'] 181 | end 182 | 183 | res.result.should == [['a', 'b'], ['a', 'b']] 184 | end 185 | 186 | it "should return the results of a flex response" do 187 | req = RocketAMF::Envelope.new :amf_version => 3 188 | req.call_flex('TestController.test', 'first_arg', 'second_arg') 189 | res = RocketAMF::Envelope.new 190 | res.each_method_call req do |method, args| 191 | ['a', 'b'] 192 | end 193 | res.result.should == ['a', 'b'] 194 | end 195 | end 196 | end -------------------------------------------------------------------------------- /lib/rocketamf/remoting.rb: -------------------------------------------------------------------------------- 1 | module RocketAMF 2 | # Container for the AMF request/response. 3 | class Envelope 4 | attr_reader :amf_version, :headers, :messages 5 | 6 | def initialize props={} 7 | @amf_version = props[:amf_version] || 0 8 | @headers = props[:headers] || {} 9 | @messages = props[:messages] || [] 10 | end 11 | 12 | # Populates the envelope from the given stream or string using the given 13 | # class mapper, or creates a new one. Returns self for easy chaining. 14 | # 15 | # Example: 16 | # 17 | # req = RocketAMF::Envelope.new.populate_from_stream(env['rack.input'].read) 18 | #-- 19 | # Implemented in pure/remoting.rb RocketAMF::Pure::Envelope 20 | def populate_from_stream stream, class_mapper=nil 21 | raise AMFError, 'Must load "rocketamf/pure"' 22 | end 23 | 24 | # Creates the appropriate message and adds it to messages to call 25 | # the given target using the standard (old) remoting APIs. You can call multiple 26 | # targets in the same request, unlike with the flex remotings APIs. 27 | # 28 | # Example: 29 | # 30 | # req = RocketAMF::Envelope.new 31 | # req.call 'test', "arg_1", ["args", "args"] 32 | # req.call 'Controller.action' 33 | def call target, *args 34 | raise "Cannot use different call types" unless @call_type.nil? || @call_type == :simple 35 | @call_type = :simple 36 | 37 | msg_num = messages.length+1 38 | @messages << RocketAMF::Message.new(target, "/#{msg_num}", args) 39 | end 40 | 41 | # Creates the appropriate message and adds it to messages using the 42 | # new flex (RemoteObject) remoting APIs. You can only make one flex remoting 43 | # call per envelope, and the AMF version must be set to 3. 44 | # 45 | # Example: 46 | # 47 | # req = RocketAMF::Envelope.new :amf_version => 3 48 | # req.call_flex 'Controller.action', "arg_1", ["args", "args"] 49 | def call_flex target, *args 50 | raise "Can only call one flex target per request" if @call_type == :flex 51 | raise "Cannot use different call types" if @call_type == :simple 52 | raise "Cannot use flex remoting calls with AMF0" if @amf_version != 3 53 | @call_type = :flex 54 | 55 | flex_msg = RocketAMF::Values::RemotingMessage.new 56 | target_parts = target.split(".") 57 | flex_msg.operation = target_parts.pop # Use pop so that a missing source is possible without issues 58 | flex_msg.source = target_parts.pop 59 | flex_msg.body = args 60 | @messages << RocketAMF::Message.new('null', '/2', flex_msg) # /2 because it always sends a command message before 61 | end 62 | 63 | # Serializes the envelope to a string using the given class mapper, or creates 64 | # a new one, and returns the result 65 | #-- 66 | # Implemented in pure/remoting.rb RocketAMF::Pure::Envelope 67 | def serialize class_mapper=nil 68 | raise AMFError, 'Must load "rocketamf/pure"' 69 | end 70 | 71 | # Builds response from the request, iterating over each method call and using 72 | # the return value as the method call's return value. Marks as envelope as 73 | # constructed after running. 74 | #-- 75 | # Iterate over all the sent messages. If they're somthing we can handle, like 76 | # a command message, then simply add the response message ourselves. If it's 77 | # a method call, then call the block with the method and args, catching errors 78 | # for handling. Then create the appropriate response message using the return 79 | # value of the block as the return value for the method call. 80 | def each_method_call request, &block 81 | raise 'Response already constructed' if @constructed 82 | 83 | # Set version from response 84 | # Can't just copy version because FMS sends version as 1 85 | @amf_version = request.amf_version == 3 ? 3 : 0 86 | 87 | request.messages.each do |m| 88 | # What's the request body? 89 | case m.data 90 | when Values::CommandMessage 91 | # Pings should be responded to with an AcknowledgeMessage built using the ping 92 | # Everything else is unsupported 93 | command_msg = m.data 94 | if command_msg.operation == Values::CommandMessage::CLIENT_PING_OPERATION 95 | response_value = Values::AcknowledgeMessage.new(command_msg) 96 | else 97 | e = Exception.new("CommandMessage #{command_msg.operation} not implemented") 98 | e.set_backtrace ["RocketAMF::Envelope each_method_call"] 99 | response_value = Values::ErrorMessage.new(command_msg, e) 100 | end 101 | when Values::RemotingMessage 102 | # Using RemoteObject style message calls 103 | remoting_msg = m.data 104 | acknowledge_msg = Values::AcknowledgeMessage.new(remoting_msg) 105 | method_base = remoting_msg.source.to_s.empty? ? '' : remoting_msg.source+'.' 106 | body = dispatch_call :method => method_base+remoting_msg.operation, :args => remoting_msg.body, :source => remoting_msg, :block => block 107 | 108 | # Response should be the bare ErrorMessage if there was an error 109 | if body.is_a?(Values::ErrorMessage) 110 | response_value = body 111 | else 112 | acknowledge_msg.body = body 113 | response_value = acknowledge_msg 114 | end 115 | else 116 | # Standard response message 117 | response_value = dispatch_call :method => m.target_uri, :args => m.data, :source => m, :block => block 118 | end 119 | 120 | target_uri = m.response_uri 121 | target_uri += response_value.is_a?(Values::ErrorMessage) ? '/onStatus' : '/onResult' 122 | @messages << ::RocketAMF::Message.new(target_uri, '', response_value) 123 | end 124 | 125 | @constructed = true 126 | end 127 | 128 | # Returns the result of a response envelope, or an array of results if there 129 | # are multiple action call messages. It automatically unwraps flex-style 130 | # RemoteObject response messages, where the response result is inside a 131 | # RocketAMF::Values::AcknowledgeMessage. 132 | # 133 | # Example: 134 | # 135 | # req = RocketAMF::Envelope.new 136 | # req.call('TestController.test', 'first_arg', 'second_arg') 137 | # res = RocketAMF::Envelope.new 138 | # res.each_method_call req do |method, args| 139 | # ['a', 'b'] 140 | # end 141 | # res.result #=> ['a', 'b'] 142 | def result 143 | results = [] 144 | messages.each do |msg| 145 | if msg.data.is_a?(Values::AcknowledgeMessage) 146 | results << msg.data.body 147 | else 148 | results << msg.data 149 | end 150 | end 151 | results.length > 1 ? results : results[0] 152 | end 153 | 154 | # Whether or not the response has been constructed. Can be used to prevent 155 | # serialization when no processing has taken place. 156 | def constructed? 157 | @constructed 158 | end 159 | 160 | # Return the serialized envelope as a string 161 | def to_s 162 | serialize 163 | end 164 | 165 | def dispatch_call p #:nodoc: 166 | begin 167 | p[:block].call(p[:method], p[:args]) 168 | rescue Exception => e 169 | # Create ErrorMessage object using the source message as the base 170 | Values::ErrorMessage.new(p[:source], e) 171 | end 172 | end 173 | end 174 | 175 | # RocketAMF::Envelope header 176 | class Header 177 | attr_accessor :name, :must_understand, :data 178 | 179 | def initialize name, must_understand, data 180 | @name = name 181 | @must_understand = must_understand 182 | @data = data 183 | end 184 | end 185 | 186 | # RocketAMF::Envelope message 187 | class Message 188 | attr_accessor :target_uri, :response_uri, :data 189 | 190 | def initialize target_uri, response_uri, data 191 | @target_uri = target_uri 192 | @response_uri = response_uri 193 | @data = data 194 | end 195 | end 196 | end -------------------------------------------------------------------------------- /spec/flash/Encoder.as: -------------------------------------------------------------------------------- 1 | package { 2 | import flash.desktop.NativeApplication; 3 | import flash.display.Sprite; 4 | import flash.events.Event; 5 | import flash.filesystem.*; 6 | import flash.net.registerClassAlias; 7 | import flash.utils.*; 8 | import flash.xml.XMLDocument; 9 | import mx.collections.ArrayCollection; 10 | 11 | public class Encoder extends Sprite { 12 | public function Encoder() { 13 | var dir:File = File.userDirectory; 14 | dir.browseForDirectory("Select Output Directory"); 15 | dir.addEventListener(Event.SELECT, writeSpecFixtures) 16 | } 17 | 18 | private function writeSpecFixtures(evt:Event):void { 19 | registerClassAlias('org.amf.ASClass', ASClass); 20 | registerClassAlias('ExternalizableTest', ExternalizableTest); 21 | registerClassAlias('flex.messaging.io.ArrayCollection', mx.collections.ArrayCollection); 22 | XML.prettyPrinting = false; 23 | 24 | var tests:Object = { 25 | 'amf0-number': 3.5, 26 | 'amf0-boolean': true, 27 | 'amf0-string': "this is a テスト", 28 | 'amf0-null': null, 29 | 'amf0-undefined': undefined, 30 | 'amf0-hash': function():Array { 31 | var a:Array = new Array(); 32 | a['a'] = 'b'; 33 | a['c'] = 'd'; 34 | return a; 35 | }, 36 | 'amf0-empty-string-key-hash': function():Array { 37 | var a:Array = new Array(); 38 | a['a'] = 'b'; 39 | a['c'] = 'd'; 40 | a[''] = 'last'; 41 | return a; 42 | }, 43 | 'amf0-ecma-ordinal-array': ['a', 'b', 'c', 'd'], 44 | //'amf0-strict-array': ['a', 'b', 'c', 'd'], // Not possible from AS3 45 | 'amf0-time': function():Date { 46 | var d:Date = new Date(); 47 | d.setTime(Date.UTC(2003, 1, 13, 5)); 48 | return d; 49 | }, 50 | 'amf0-date': function():Date { 51 | var d:Date = new Date(); 52 | d.setTime(Date.UTC(2020, 4, 30)); 53 | return d; 54 | }, 55 | 'amf0-xml-doc': new XMLDocument(''), 56 | 'amf0-object': function():Object { 57 | var o:Object = {}; 58 | o['bar'] = 3.14; 59 | o['foo'] = 'baz'; 60 | return o; 61 | }, 62 | 'amf0-untyped-object': function():Object { 63 | var o:Object = {}; 64 | o['baz'] = null; 65 | o['foo'] = 'bar'; 66 | return o; 67 | }, 68 | 'amf0-typed-object': new ASClass('bar'), 69 | 'amf0-ref-test': function():Object { 70 | var o:Object = tests['amf0-object'](); 71 | var ret:Object = {}; 72 | ret['0'] = o; 73 | ret['1'] = o; 74 | return ret; 75 | }, 76 | 'amf0-complex-encoded-string': function():Object { 77 | var o:Object = {}; 78 | o['shift'] = "Shift テスト"; 79 | o['utf'] = "UTF テスト"; 80 | o['zed'] = 5; 81 | return o; 82 | }, 83 | 'amf3-null': null, 84 | 'amf3-false': false, 85 | 'amf3-true': true, 86 | 'amf3-max': 268435455, 87 | 'amf3-0': 0, 88 | 'amf3-min': -268435456, 89 | 'amf3-float': 3.5, 90 | 'amf3-large-max': 268435456, 91 | 'amf3-large-min': -268435457, 92 | 'amf3-bignum': Math.pow(2, 1000), 93 | 'amf3-string': "String . String", 94 | 'amf3-symbol': "foo", 95 | 'amf3-date': function():Date { 96 | var d:Date = new Date(); 97 | d.setTime(0); 98 | return d; 99 | }, 100 | 'amf3-xml': new XML(''), 101 | 'amf3-xml-doc': new XMLDocument(''), 102 | 'amf3-dynamic-object': function():Object { 103 | var o:Object = {}; 104 | o['another_public_property'] = 'a_public_value'; 105 | o['nil_property'] = null; 106 | o['property_one'] = 'foo'; 107 | return o; 108 | }, 109 | 'amf3-typed-object': new ASClass('bar'), 110 | 'amf3-externalizable': [new ExternalizableTest(5, 7), new ExternalizableTest(13, 5)], 111 | 'amf3-hash': function():Object { 112 | var o:Object = {}; 113 | o['answer'] = 42; 114 | o['foo'] = 'bar'; 115 | return o; 116 | }, 117 | 'amf3-empty-array': [], 118 | 'amf3-primitive-array': [1,2,3,4,5], 119 | 'amf3-associative-array': function():Array { 120 | var a:Array = []; 121 | a["asdf"] = "fdsa"; 122 | a["foo"] = "bar"; 123 | a[42] = "bar"; 124 | a[0] = "bar1"; 125 | a[1] = "bar2"; 126 | a[2] = "bar3"; 127 | return a; 128 | }, 129 | 'amf3-mixed-array': function():Array { 130 | var h1:Object = {"foo_one": "bar_one"}; 131 | var h2:Object = {"foo_two": ""}; 132 | var so1:Object = {"foo_three": 42}; 133 | return [h1, h2, so1, {}, [h1, h2, so1], [], 42, "", [], "", {}, "bar_one", so1]; 134 | }, 135 | 'amf3-array-collection': new ArrayCollection(['foo', 'bar']), 136 | 'amf3-complex-array-collection': function():Array { 137 | var a:ArrayCollection = new ArrayCollection(['foo', 'bar']); 138 | var b:ArrayCollection = new ArrayCollection([new ASClass('bar'), new ASClass('asdf')]); 139 | return [a, b, b]; 140 | }, 141 | 'amf3-byte-array': function():ByteArray { 142 | var b:ByteArray = new ByteArray(); 143 | b.writeByte(0); 144 | b.writeByte(3); 145 | b.writeUTFBytes("これtest"); 146 | b.writeByte(64); 147 | return b; 148 | }, 149 | 'amf3-empty-dictionary': new Dictionary(), 150 | 'amf3-dictionary': function():Dictionary { 151 | var d:Dictionary = new Dictionary(); 152 | d["bar"] = "asdf1"; 153 | d[new ASClass("baz")] = "asdf2"; 154 | return d; 155 | }, 156 | 'amf3-string-ref': function():Array { 157 | var foo:String = "foo"; 158 | var bar:String = "str"; 159 | return [foo, bar, foo, bar, foo, {"str": foo}]; 160 | }, 161 | 'amf3-empty-string-ref': function():Array { 162 | var s:String = ""; 163 | return [s, s]; 164 | }, 165 | 'amf3-date-ref': function():Array { 166 | var d:Date = new Date(); 167 | d.setTime(0); 168 | return [d, d]; 169 | }, 170 | 'amf3-object-ref': function():Array { 171 | var obj1:Object = {"foo": "bar"}; 172 | var obj2:Object = {"foo": obj1["foo"]}; 173 | return [[obj1, obj2], "bar", [obj1, obj2]]; 174 | }, 175 | 'amf3-trait-ref': [new ASClass("foo"), new ASClass("bar")], 176 | 'amf3-array-ref': function():Array { 177 | var a:Array = [1, 2, 3]; 178 | var b:Array = ['a', 'b', 'c']; 179 | return [a, b, a, b]; 180 | }, 181 | 'amf3-empty-array-ref': function():Array { 182 | var a:Array = []; var b:Array = []; 183 | return [a, b, a, b]; 184 | }, 185 | 'amf3-xml-ref': function():Array { 186 | var x:XML = new XML(''); 187 | return [x, x]; 188 | }, 189 | 'amf3-byte-array-ref': function():Array { 190 | var b:ByteArray = new ByteArray(); 191 | b.writeUTFBytes("ASDF"); 192 | return [b, b]; 193 | }, 194 | 'amf3-graph-member': function():Object { 195 | var parentObj:Object = {}; 196 | var child1:Object = {"children": []}; 197 | child1['parent'] = parentObj; 198 | var child2:Object = {"children": []}; 199 | child2['parent'] = parentObj; 200 | parentObj['children'] = [child1, child2]; 201 | parentObj['parent'] = null; 202 | return parentObj; 203 | }, 204 | 'amf3-complex-encoded-string-array': [5, "Shift テスト", "UTF テスト", 5], 205 | 'amf3-encoded-string-ref': ["this is a テスト", "this is a テスト"], 206 | 'amf3-vector-int': function():Vector. { 207 | var v:Vector. = new Vector.(); 208 | v.push(4); 209 | v.push(-20); 210 | v.push(12); 211 | return v; 212 | }, 213 | 'amf3-vector-uint': function():Vector. { 214 | var v:Vector. = new Vector.(); 215 | v.push(4); 216 | v.push(20); 217 | v.push(12); 218 | return v; 219 | }, 220 | 'amf3-vector-double': function():Vector. { 221 | var v:Vector. = new Vector.(); 222 | v.push(4.3); 223 | v.push(-20.6); 224 | return v; 225 | }, 226 | 'amf3-vector-object': function():Vector. { 227 | var v:Vector. = new Vector.(); 228 | v.push(new ASClass('foo')); 229 | v.push(new ASClass('bar')); 230 | v.push(new ASClass('baz')); 231 | return v; 232 | } 233 | }; 234 | 235 | var outputDir:File = evt.target as File; 236 | for(var key:String in tests) { 237 | trace(key); 238 | var fs:FileStream = new FileStream(); 239 | fs.objectEncoding = (key.indexOf('amf0-') === 0) ? 0 : 3; 240 | fs.open(outputDir.resolvePath(key+'.bin'), FileMode.WRITE); 241 | fs.writeObject(tests[key] is Function ? tests[key]() : tests[key]); 242 | fs.close(); 243 | } 244 | 245 | NativeApplication.nativeApplication.exit(); 246 | } 247 | } 248 | } -------------------------------------------------------------------------------- /lib/rocketamf.rb: -------------------------------------------------------------------------------- 1 | $:.unshift(File.dirname(__FILE__)) unless $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__))) 2 | $:.unshift "#{File.expand_path(File.dirname(__FILE__))}/rocketamf/" 3 | 4 | require "date" 5 | require "stringio" 6 | require 'rocketamf/extensions' 7 | require 'rocketamf/class_mapping' 8 | require 'rocketamf/constants' 9 | require 'rocketamf/remoting' 10 | 11 | # RocketAMF is a full featured AMF0/3 serializer and deserializer with support for 12 | # bi-directional Flash to Ruby class mapping, custom serialization and mapping, 13 | # remoting gateway helpers that follow AMF0/3 messaging specs, and a suite of specs 14 | # to ensure adherence to the specification documents put out by Adobe. If the C 15 | # components compile, then RocketAMF automatically takes advantage of them to 16 | # provide a substantial performance benefit. In addition, RocketAMF is fully 17 | # compatible with Ruby 1.9. 18 | # 19 | # == Performance 20 | # 21 | # RocketAMF provides native C extensions for serialization, deserialization, 22 | # remoting, and class mapping. If your environment supports them, RocketAMF will 23 | # automatically take advantage of the C serializer, deserializer, and remoting 24 | # support. The C class mapper has some substantial performance optimizations that 25 | # make it incompatible with the pure Ruby class mapper, and so it must be manually 26 | # enabled. For more information see RocketAMF::ClassMapping. Below are 27 | # some benchmarks I took using using a simple little benchmarking utility I whipped 28 | # up, which can be found in the root of the repository. 29 | # 30 | # # 100000 objects 31 | # # Ruby 1.8 32 | # Testing native AMF0: 33 | # minimum serialize time: 1.229868s 34 | # minimum deserialize time: 0.86465s 35 | # Testing native AMF3: 36 | # minimum serialize time: 1.444652s 37 | # minimum deserialize time: 0.879407s 38 | # Testing pure AMF0: 39 | # minimum serialize time: 25.427931s 40 | # minimum deserialize time: 11.706084s 41 | # Testing pure AMF3: 42 | # minimum serialize time: 31.637864s 43 | # minimum deserialize time: 14.773969s 44 | # 45 | # == Serialization & Deserialization 46 | # 47 | # RocketAMF provides two main methods - serialize and deserialize. 48 | # Deserialization takes a String or StringIO object and the AMF version if different 49 | # from the default. Serialization takes any Ruby object and the version if different 50 | # from the default. Both default to AMF0, as it's more widely supported and slightly 51 | # faster, but AMF3 does a better job of not sending duplicate data. Which you choose 52 | # depends on what you need to communicate with and how much serialized size matters. 53 | # 54 | # == Mapping Classes Between Flash and Ruby 55 | # 56 | # RocketAMF provides a simple class mapping tool to facilitate serialization and 57 | # deserialization of typed objects. Refer to the documentation of 58 | # RocketAMF::ClassMapping for more details. If the provided class 59 | # mapping tool is not sufficient for your needs, you also have the option to 60 | # replace it with a class mapper of your own devising that matches the documented 61 | # API. 62 | # 63 | # == Remoting 64 | # 65 | # You can use RocketAMF bare to write an AMF gateway using the following code. 66 | # In addition, you can use rack-amf (http://github.com/rubyamf/rack-amf) or 67 | # RubyAMF (http://github.com/rubyamf/rubyamf), both of which provide rack-compliant 68 | # AMF gateways. 69 | # 70 | # # helloworld.ru 71 | # require 'rocketamf' 72 | # 73 | # class HelloWorldApp 74 | # APPLICATION_AMF = 'application/x-amf'.freeze 75 | # 76 | # def call env 77 | # if is_amf?(env) 78 | # # Wrap request and response 79 | # env['rack.input'].rewind 80 | # request = RocketAMF::Envelope.new.populate_from_stream(env['rack.input'].read) 81 | # response = RocketAMF::Envelope.new 82 | # 83 | # # Handle request 84 | # response.each_method_call request do |method, args| 85 | # raise "Service #{method} does not exists" unless method == 'App.helloWorld' 86 | # 'Hello world' 87 | # end 88 | # 89 | # # Pass back response 90 | # response_str = response.serialize 91 | # return [200, {'Content-Type' => APPLICATION_AMF, 'Content-Length' => response_str.length.to_s}, [response_str]] 92 | # else 93 | # return [200, {'Content-Type' => 'text/plain', 'Content-Length' => '16' }, ["Rack AMF gateway"]] 94 | # end 95 | # end 96 | # 97 | # private 98 | # def is_amf? env 99 | # return false unless env['CONTENT_TYPE'] == APPLICATION_AMF 100 | # return false unless env['PATH_INFO'] == '/amf' 101 | # return true 102 | # end 103 | # end 104 | # 105 | # run HelloWorldApp.new 106 | # 107 | # == Advanced Serialization (encode_amf and IExternalizable) 108 | # 109 | # RocketAMF provides some additional functionality to support advanced 110 | # serialization techniques. If you define an encode_amf method on your 111 | # object, it will get called during serialization. It is passed a single argument, 112 | # the serializer, and it can use the serializer stream, the serialize 113 | # method, the write_array method, the write_object method, and 114 | # the serializer version. Below is a simple example that uses write_object 115 | # to customize the property hash that is used for serialization. 116 | # 117 | # Example: 118 | # 119 | # class TestObject 120 | # def encode_amf ser 121 | # ser.write_object self, @attributes 122 | # end 123 | # end 124 | # 125 | # If you plan on using the serialize method, make sure to pass in the 126 | # current serializer version, or you could create a message that cannot be deserialized. 127 | # 128 | # Example: 129 | # 130 | # class VariableObject 131 | # def encode_amf ser 132 | # if ser.version == 0 133 | # ser.serialize 0, true 134 | # else 135 | # ser.serialize 3, false 136 | # end 137 | # end 138 | # end 139 | # 140 | # If you wish to send and receive IExternalizable objects, you will need to 141 | # implement encode_amf, read_external, and write_external. 142 | # Below is an example of a ResultSet class that extends Array and serializes as 143 | # an array collection. RocketAMF can automatically serialize arrays as 144 | # ArrayCollection objects, so this is just an example of how you might implement 145 | # an object that conforms to IExternalizable. 146 | # 147 | # Example: 148 | # 149 | # class ResultSet < Array 150 | # def encode_amf ser 151 | # if ser.version == 0 152 | # # Serialize as simple array in AMF0 153 | # ser.write_array self 154 | # else 155 | # # Serialize as an ArrayCollection object 156 | # # It conforms to IExternalizable, does not have any dynamic properties, 157 | # # and has no "sealed" members. See the AMF3 specs for more details about 158 | # # object traits. 159 | # ser.write_object self, nil, { 160 | # :class_name => "flex.messaging.io.ArrayCollection", 161 | # :externalizable => true, 162 | # :dynamic => false, 163 | # :members => [] 164 | # } 165 | # end 166 | # end 167 | # 168 | # # Write self as array to stream 169 | # def write_external ser 170 | # ser.write_array(self) 171 | # end 172 | # 173 | # # Read array out and replace data with deserialized array. 174 | # def read_external des 175 | # replace(des.read_object) 176 | # end 177 | # end 178 | module RocketAMF 179 | begin 180 | require 'rocketamf/ext' 181 | rescue LoadError 182 | require 'rocketamf/pure' 183 | end 184 | 185 | # Deserialize the AMF string _source_ of the given AMF version into a Ruby 186 | # data structure and return it. Creates an instance of RocketAMF::Deserializer 187 | # with a new instance of RocketAMF::ClassMapper and calls deserialize 188 | # on it with the given source and amf version, returning the result. 189 | def self.deserialize source, amf_version = 0 190 | des = RocketAMF::Deserializer.new(RocketAMF::ClassMapper.new) 191 | des.deserialize(amf_version, source) 192 | end 193 | 194 | # Serialize the given Ruby data structure _obj_ into an AMF stream using the 195 | # given AMF version. Creates an instance of RocketAMF::Serializer 196 | # with a new instance of RocketAMF::ClassMapper and calls serialize 197 | # on it with the given object and amf version, returning the result. 198 | def self.serialize obj, amf_version = 0 199 | ser = RocketAMF::Serializer.new(RocketAMF::ClassMapper.new) 200 | ser.serialize(amf_version, obj) 201 | end 202 | 203 | # We use const_missing to define the active ClassMapper at runtime. This way, 204 | # heavy modification of class mapping functionality is still possible without 205 | # forcing extenders to redefine the constant. 206 | def self.const_missing const #:nodoc: 207 | if const == :ClassMapper 208 | RocketAMF.const_set(:ClassMapper, RocketAMF::ClassMapping) 209 | else 210 | super(const) 211 | end 212 | end 213 | 214 | # The base exception for AMF errors. 215 | class AMFError < StandardError; end 216 | end -------------------------------------------------------------------------------- /lib/rocketamf/class_mapping.rb: -------------------------------------------------------------------------------- 1 | require 'rocketamf/values/typed_hash' 2 | require 'rocketamf/values/messages' 3 | 4 | module RocketAMF 5 | # Container for all mapped classes 6 | class MappingSet 7 | # Creates a mapping set object and populates the default mappings 8 | def initialize 9 | @as_mappings = {} 10 | @ruby_mappings = {} 11 | map_defaults 12 | end 13 | 14 | # Adds required mapping configs, calling map for the required base mappings. 15 | # Designed to allow extenders to take advantage of required default mappings. 16 | def map_defaults 17 | map :as => 'flex.messaging.messages.AbstractMessage', :ruby => 'RocketAMF::Values::AbstractMessage' 18 | map :as => 'flex.messaging.messages.RemotingMessage', :ruby => 'RocketAMF::Values::RemotingMessage' 19 | map :as => 'flex.messaging.messages.AsyncMessage', :ruby => 'RocketAMF::Values::AsyncMessage' 20 | map :as => 'DSA', :ruby => 'RocketAMF::Values::AsyncMessageExt' 21 | map :as => 'flex.messaging.messages.CommandMessage', :ruby => 'RocketAMF::Values::CommandMessage' 22 | map :as => 'DSC', :ruby => 'RocketAMF::Values::CommandMessageExt' 23 | map :as => 'flex.messaging.messages.AcknowledgeMessage', :ruby => 'RocketAMF::Values::AcknowledgeMessage' 24 | map :as => 'DSK', :ruby => 'RocketAMF::Values::AcknowledgeMessageExt' 25 | map :as => 'flex.messaging.messages.ErrorMessage', :ruby => 'RocketAMF::Values::ErrorMessage' 26 | self 27 | end 28 | 29 | # Map a given AS class to a ruby class. 30 | # 31 | # Use fully qualified names for both. 32 | # 33 | # Example: 34 | # 35 | # m.map :as => 'com.example.Date', :ruby => 'Example::Date' 36 | def map params 37 | [:as, :ruby].each {|k| params[k] = params[k].to_s} # Convert params to strings 38 | @as_mappings[params[:as]] = params[:ruby] 39 | @ruby_mappings[params[:ruby]] = params[:as] 40 | end 41 | 42 | # Returns the AS class name for the given ruby class name, returing nil if 43 | # not found 44 | def get_as_class_name class_name #:nodoc: 45 | @ruby_mappings[class_name.to_s] 46 | end 47 | 48 | # Returns the ruby class name for the given AS class name, returing nil if 49 | # not found 50 | def get_ruby_class_name class_name #:nodoc: 51 | @as_mappings[class_name.to_s] 52 | end 53 | end 54 | 55 | # Handles class name mapping between actionscript and ruby and assists in 56 | # serializing and deserializing data between them. Simply map an AS class to a 57 | # ruby class and when the object is (de)serialized it will end up as the 58 | # appropriate class. 59 | # 60 | # Example: 61 | # 62 | # RocketAMF::ClassMapper.define do |m| 63 | # m.map :as => 'AsClass', :ruby => 'RubyClass' 64 | # m.map :as => 'vo.User', :ruby => 'Model::User' 65 | # end 66 | # 67 | # == Object Population/Serialization 68 | # 69 | # In addition to handling class name mapping, it also provides helper methods 70 | # for populating ruby objects from AMF and extracting properties from ruby objects 71 | # for serialization. Support for hash-like objects and objects using 72 | # attr_accessor for properties is currently built in, but custom classes 73 | # may require subclassing the class mapper to add support. 74 | # 75 | # == Complete Replacement 76 | # 77 | # In some cases, it may be beneficial to replace the default provider of class 78 | # mapping completely. In this case, simply assign your class mapper class to 79 | # RocketAMF::ClassMapper after loading RocketAMF. Through the magic of 80 | # const_missing, ClassMapper is only defined after the first 81 | # access by default, so you get no annoying warning messages. Custom class mappers 82 | # must implement the following methods on instances: use_array_collection, 83 | # get_as_class_name, get_ruby_obj, populate_ruby_obj, 84 | # and props_for_serialization. In addition, it should have a class level 85 | # mappings method that returns the mapping set it's using, although its 86 | # not required. If you'd like to see an example of what complete replacement 87 | # offers, check out RubyAMF (http://github.com/rubyamf/rubyamf). 88 | # 89 | # Example: 90 | # 91 | # require 'rubygems' 92 | # require 'rocketamf' 93 | # 94 | # RocketAMF::ClassMapper = MyCustomClassMapper 95 | # # No warning about already initialized constant ClassMapper 96 | # RocketAMF::ClassMapper # MyCustomClassMapper 97 | # 98 | # == C ClassMapper 99 | # 100 | # The C class mapper, RocketAMF::Ext::FastClassMapping, has the same 101 | # public API that RubyAMF::ClassMapping does, but has some additional 102 | # performance optimizations that may interfere with the proper serialization of 103 | # objects. To reduce the cost of processing public methods for every object, 104 | # its implementation of props_for_serialization caches valid properties 105 | # by class, using the class as the hash key for property lookup. This means that 106 | # adding and removing properties from instances while serializing using a given 107 | # class mapper instance will result in the changes not being detected. As such, 108 | # it's not enabled by default. So long as you aren't planning on modifying 109 | # classes during serialization using encode_amf, the faster C class 110 | # mapper should be perfectly safe to use. 111 | # 112 | # Activating the C Class Mapper: 113 | # 114 | # require 'rubygems' 115 | # require 'rocketamf' 116 | # RocketAMF::ClassMapper = RocketAMF::Ext::FastClassMapping 117 | class ClassMapping 118 | class << self 119 | # Global configuration variable for sending Arrays as ArrayCollections. 120 | # Defaults to false. 121 | attr_accessor :use_array_collection 122 | 123 | # Returns the mapping set with all the class mappings that is currently 124 | # being used. 125 | def mappings 126 | @mappings ||= MappingSet.new 127 | end 128 | 129 | # Define class mappings in the block. Block is passed a MappingSet object 130 | # as the first parameter. 131 | # 132 | # Example: 133 | # 134 | # RocketAMF::ClassMapper.define do |m| 135 | # m.map :as => 'AsClass', :ruby => 'RubyClass' 136 | # end 137 | def define &block #:yields: mapping_set 138 | yield mappings 139 | end 140 | 141 | # Reset all class mappings except the defaults and return 142 | # use_array_collection to false 143 | def reset 144 | @use_array_collection = false 145 | @mappings = nil 146 | end 147 | end 148 | 149 | attr_reader :use_array_collection 150 | 151 | # Copies configuration from class level configs to populate object 152 | def initialize 153 | @mappings = self.class.mappings 154 | @use_array_collection = self.class.use_array_collection === true 155 | end 156 | 157 | # Returns the ActionScript class name for the given ruby object. Will also 158 | # take a string containing the ruby class name. 159 | def get_as_class_name obj 160 | # Get class name 161 | if obj.is_a?(String) 162 | ruby_class_name = obj 163 | elsif obj.is_a?(Values::TypedHash) 164 | ruby_class_name = obj.type 165 | elsif obj.is_a?(Hash) 166 | return nil 167 | else 168 | ruby_class_name = obj.class.name 169 | end 170 | 171 | # Get mapped AS class name 172 | @mappings.get_as_class_name ruby_class_name 173 | end 174 | 175 | # Instantiates a ruby object using the mapping configuration based on the 176 | # source ActionScript class name. If there is no mapping defined, it returns 177 | # a RocketAMF::Values::TypedHash with the serialized class name. 178 | def get_ruby_obj as_class_name 179 | ruby_class_name = @mappings.get_ruby_class_name as_class_name 180 | if ruby_class_name.nil? 181 | # Populate a simple hash, since no mapping 182 | return Values::TypedHash.new(as_class_name) 183 | else 184 | ruby_class = ruby_class_name.split('::').inject(Kernel) {|scope, const_name| scope.const_get(const_name)} 185 | return ruby_class.new 186 | end 187 | end 188 | 189 | # Populates the ruby object using the given properties. props and 190 | # dynamic_props will be hashes with symbols for keys. 191 | def populate_ruby_obj obj, props, dynamic_props=nil 192 | props.merge! dynamic_props if dynamic_props 193 | 194 | # Don't even bother checking if it responds to setter methods if it's a TypedHash 195 | if obj.is_a?(Values::TypedHash) 196 | obj.merge! props 197 | return obj 198 | end 199 | 200 | # Some type of object 201 | hash_like = obj.respond_to?("[]=") 202 | props.each do |key, value| 203 | if obj.respond_to?("#{key}=") 204 | obj.send("#{key}=", value) 205 | elsif hash_like 206 | obj[key] = value 207 | end 208 | end 209 | obj 210 | end 211 | 212 | # Extracts all exportable properties from the given ruby object and returns 213 | # them in a hash. If overriding, make sure to return a hash wth string keys 214 | # unless you are only going to be using the native C extensions, as the pure 215 | # ruby serializer performs a sort on the keys to acheive consistent, testable 216 | # results. 217 | def props_for_serialization ruby_obj 218 | # Handle hashes 219 | if ruby_obj.is_a?(Hash) 220 | # Stringify keys to make it easier later on and allow sorting 221 | h = {} 222 | ruby_obj.each {|k,v| h[k.to_s] = v} 223 | return h 224 | end 225 | 226 | # Generic object serializer 227 | props = {} 228 | @ignored_props ||= Object.new.public_methods 229 | (ruby_obj.public_methods - @ignored_props).each do |method_name| 230 | # Add them to the prop hash if they take no arguments 231 | method_def = ruby_obj.method(method_name) 232 | props[method_name.to_s] = ruby_obj.send(method_name) if method_def.arity == 0 233 | end 234 | props 235 | end 236 | end 237 | end -------------------------------------------------------------------------------- /lib/rocketamf/pure/deserializer.rb: -------------------------------------------------------------------------------- 1 | require 'rocketamf/pure/io_helpers' 2 | 3 | module RocketAMF 4 | module Pure 5 | # Pure ruby deserializer for AMF0 and AMF3 6 | class Deserializer 7 | attr_accessor :source 8 | 9 | # Pass in the class mapper instance to use when deserializing. This 10 | # enables better caching behavior in the class mapper and allows 11 | # one to change mappings between deserialization attempts. 12 | def initialize class_mapper 13 | @class_mapper = class_mapper 14 | end 15 | 16 | # Deserialize the source using AMF0 or AMF3. Source should either 17 | # be a string or StringIO object. If you pass a StringIO object, 18 | # it will have its position updated to the end of the deserialized 19 | # data. 20 | def deserialize version, source 21 | raise ArgumentError, "unsupported version #{version}" unless [0,3].include?(version) 22 | @version = version 23 | 24 | if StringIO === source 25 | @source = source 26 | elsif source 27 | @source = StringIO.new(source) 28 | elsif @source.nil? 29 | raise AMFError, "no source to deserialize" 30 | end 31 | 32 | if @version == 0 33 | @ref_cache = [] 34 | return amf0_deserialize 35 | else 36 | @string_cache = [] 37 | @object_cache = [] 38 | @trait_cache = [] 39 | return amf3_deserialize 40 | end 41 | end 42 | 43 | # Reads an object from the deserializer's stream and returns it. 44 | def read_object 45 | if @version == 0 46 | return amf0_deserialize 47 | else 48 | return amf3_deserialize 49 | end 50 | end 51 | 52 | private 53 | include RocketAMF::Pure::ReadIOHelpers 54 | 55 | def amf0_deserialize type=nil 56 | type = read_int8 @source unless type 57 | case type 58 | when AMF0_NUMBER_MARKER 59 | amf0_read_number 60 | when AMF0_BOOLEAN_MARKER 61 | amf0_read_boolean 62 | when AMF0_STRING_MARKER 63 | amf0_read_string 64 | when AMF0_OBJECT_MARKER 65 | amf0_read_object 66 | when AMF0_NULL_MARKER 67 | nil 68 | when AMF0_UNDEFINED_MARKER 69 | nil 70 | when AMF0_REFERENCE_MARKER 71 | amf0_read_reference 72 | when AMF0_HASH_MARKER 73 | amf0_read_hash 74 | when AMF0_STRICT_ARRAY_MARKER 75 | amf0_read_array 76 | when AMF0_DATE_MARKER 77 | amf0_read_date 78 | when AMF0_LONG_STRING_MARKER 79 | amf0_read_string true 80 | when AMF0_UNSUPPORTED_MARKER 81 | nil 82 | when AMF0_XML_MARKER 83 | amf0_read_string true 84 | when AMF0_TYPED_OBJECT_MARKER 85 | amf0_read_typed_object 86 | when AMF0_AMF3_MARKER 87 | deserialize(3, nil) 88 | else 89 | raise AMFError, "Invalid type: #{type}" 90 | end 91 | end 92 | 93 | def amf0_read_number 94 | res = read_double @source 95 | (res.is_a?(Float) && res.nan?) ? nil : res # check for NaN and convert them to nil 96 | end 97 | 98 | def amf0_read_boolean 99 | read_int8(@source) != 0 100 | end 101 | 102 | def amf0_read_string long=false 103 | len = long ? read_word32_network(@source) : read_word16_network(@source) 104 | str = @source.read(len) 105 | str.force_encoding("UTF-8") if str.respond_to?(:force_encoding) 106 | str 107 | end 108 | 109 | def amf0_read_reference 110 | index = read_word16_network(@source) 111 | @ref_cache[index] 112 | end 113 | 114 | def amf0_read_array 115 | len = read_word32_network(@source) 116 | array = [] 117 | @ref_cache << array 118 | 119 | 0.upto(len - 1) do 120 | array << amf0_deserialize 121 | end 122 | array 123 | end 124 | 125 | def amf0_read_date 126 | seconds = read_double(@source).to_f/1000 127 | time = Time.at(seconds) 128 | tz = read_word16_network(@source) # Unused 129 | time 130 | end 131 | 132 | def amf0_read_props obj={} 133 | while true 134 | key = amf0_read_string 135 | type = read_int8 @source 136 | break if type == AMF0_OBJECT_END_MARKER 137 | obj[key] = amf0_deserialize(type) 138 | end 139 | obj 140 | end 141 | 142 | def amf0_read_hash 143 | len = read_word32_network(@source) # Read and ignore length 144 | obj = {} 145 | @ref_cache << obj 146 | amf0_read_props obj 147 | end 148 | 149 | def amf0_read_object add_to_ref_cache=true 150 | # Create "object" and add to ref cache (it's always a Hash) 151 | obj = @class_mapper.get_ruby_obj "" 152 | @ref_cache << obj 153 | 154 | # Populate object 155 | props = amf0_read_props 156 | @class_mapper.populate_ruby_obj obj, props 157 | return obj 158 | end 159 | 160 | def amf0_read_typed_object 161 | # Create object to add to ref cache 162 | class_name = amf0_read_string 163 | obj = @class_mapper.get_ruby_obj class_name 164 | @ref_cache << obj 165 | 166 | # Populate object 167 | props = amf0_read_props 168 | @class_mapper.populate_ruby_obj obj, props 169 | return obj 170 | end 171 | 172 | def amf3_deserialize 173 | type = read_int8 @source 174 | case type 175 | when AMF3_UNDEFINED_MARKER 176 | nil 177 | when AMF3_NULL_MARKER 178 | nil 179 | when AMF3_FALSE_MARKER 180 | false 181 | when AMF3_TRUE_MARKER 182 | true 183 | when AMF3_INTEGER_MARKER 184 | amf3_read_integer 185 | when AMF3_DOUBLE_MARKER 186 | amf3_read_number 187 | when AMF3_STRING_MARKER 188 | amf3_read_string 189 | when AMF3_XML_DOC_MARKER, AMF3_XML_MARKER 190 | amf3_read_xml 191 | when AMF3_DATE_MARKER 192 | amf3_read_date 193 | when AMF3_ARRAY_MARKER 194 | amf3_read_array 195 | when AMF3_OBJECT_MARKER 196 | amf3_read_object 197 | when AMF3_BYTE_ARRAY_MARKER 198 | amf3_read_byte_array 199 | when AMF3_VECTOR_INT_MARKER, AMF3_VECTOR_UINT_MARKER, AMF3_VECTOR_DOUBLE_MARKER, AMF3_VECTOR_OBJECT_MARKER 200 | amf3_read_vector type 201 | when AMF3_DICT_MARKER 202 | amf3_read_dict 203 | else 204 | raise AMFError, "Invalid type: #{type}" 205 | end 206 | end 207 | 208 | def amf3_read_integer 209 | n = 0 210 | b = read_word8(@source) || 0 211 | result = 0 212 | 213 | while ((b & 0x80) != 0 && n < 3) 214 | result = result << 7 215 | result = result | (b & 0x7f) 216 | b = read_word8(@source) || 0 217 | n = n + 1 218 | end 219 | 220 | if (n < 3) 221 | result = result << 7 222 | result = result | b 223 | else 224 | #Use all 8 bits from the 4th byte 225 | result = result << 8 226 | result = result | b 227 | 228 | #Check if the integer should be negative 229 | if (result > MAX_INTEGER) 230 | result -= (1 << 29) 231 | end 232 | end 233 | result 234 | end 235 | 236 | def amf3_read_number 237 | res = read_double @source 238 | (res.is_a?(Float) && res.nan?) ? nil : res # check for NaN and convert them to nil 239 | end 240 | 241 | def amf3_read_string 242 | type = amf3_read_integer 243 | is_reference = (type & 0x01) == 0 244 | 245 | if is_reference 246 | reference = type >> 1 247 | return @string_cache[reference] 248 | else 249 | length = type >> 1 250 | str = "" 251 | if length > 0 252 | str = @source.read(length) 253 | str.force_encoding("UTF-8") if str.respond_to?(:force_encoding) 254 | @string_cache << str 255 | end 256 | return str 257 | end 258 | end 259 | 260 | def amf3_read_xml 261 | type = amf3_read_integer 262 | is_reference = (type & 0x01) == 0 263 | 264 | if is_reference 265 | reference = type >> 1 266 | return @object_cache[reference] 267 | else 268 | length = type >> 1 269 | str = "" 270 | if length > 0 271 | str = @source.read(length) 272 | str.force_encoding("UTF-8") if str.respond_to?(:force_encoding) 273 | @object_cache << str 274 | end 275 | return str 276 | end 277 | end 278 | 279 | def amf3_read_byte_array 280 | type = amf3_read_integer 281 | is_reference = (type & 0x01) == 0 282 | 283 | if is_reference 284 | reference = type >> 1 285 | return @object_cache[reference] 286 | else 287 | length = type >> 1 288 | obj = StringIO.new @source.read(length) 289 | @object_cache << obj 290 | obj 291 | end 292 | end 293 | 294 | def amf3_read_array 295 | type = amf3_read_integer 296 | is_reference = (type & 0x01) == 0 297 | 298 | if is_reference 299 | reference = type >> 1 300 | return @object_cache[reference] 301 | else 302 | length = type >> 1 303 | property_name = amf3_read_string 304 | array = property_name.length > 0 ? {} : [] 305 | @object_cache << array 306 | 307 | while property_name.length > 0 308 | value = amf3_deserialize 309 | array[property_name] = value 310 | property_name = amf3_read_string 311 | end 312 | 0.upto(length - 1) {|i| array[i] = amf3_deserialize } 313 | 314 | array 315 | end 316 | end 317 | 318 | def amf3_read_object 319 | type = amf3_read_integer 320 | is_reference = (type & 0x01) == 0 321 | 322 | if is_reference 323 | reference = type >> 1 324 | return @object_cache[reference] 325 | else 326 | class_type = type >> 1 327 | class_is_reference = (class_type & 0x01) == 0 328 | 329 | if class_is_reference 330 | reference = class_type >> 1 331 | traits = @trait_cache[reference] 332 | else 333 | externalizable = (class_type & 0x02) != 0 334 | dynamic = (class_type & 0x04) != 0 335 | attribute_count = class_type >> 3 336 | class_name = amf3_read_string 337 | 338 | class_attributes = [] 339 | attribute_count.times{class_attributes << amf3_read_string} # Read class members 340 | 341 | traits = { 342 | :class_name => class_name, 343 | :members => class_attributes, 344 | :externalizable => externalizable, 345 | :dynamic => dynamic 346 | } 347 | @trait_cache << traits 348 | end 349 | 350 | # Optimization for deserializing ArrayCollection 351 | if traits[:class_name] == "flex.messaging.io.ArrayCollection" 352 | arr = amf3_deserialize # Adds ArrayCollection array to object cache 353 | @object_cache << arr # Add again for ArrayCollection source array 354 | return arr 355 | end 356 | 357 | obj = @class_mapper.get_ruby_obj traits[:class_name] 358 | @object_cache << obj 359 | 360 | if traits[:externalizable] 361 | obj.read_external self 362 | else 363 | props = {} 364 | traits[:members].each do |key| 365 | value = amf3_deserialize 366 | props[key] = value 367 | end 368 | 369 | dynamic_props = nil 370 | if traits[:dynamic] 371 | dynamic_props = {} 372 | while (key = amf3_read_string) && key.length != 0 do # read next key 373 | value = amf3_deserialize 374 | dynamic_props[key] = value 375 | end 376 | end 377 | 378 | @class_mapper.populate_ruby_obj obj, props, dynamic_props 379 | end 380 | obj 381 | end 382 | end 383 | 384 | def amf3_read_date 385 | type = amf3_read_integer 386 | is_reference = (type & 0x01) == 0 387 | if is_reference 388 | reference = type >> 1 389 | return @object_cache[reference] 390 | else 391 | seconds = read_double(@source).to_f/1000 392 | time = Time.at(seconds) 393 | @object_cache << time 394 | time 395 | end 396 | end 397 | 398 | def amf3_read_dict 399 | type = amf3_read_integer 400 | is_reference = (type & 0x01) == 0 401 | if is_reference 402 | reference = type >> 1 403 | return @object_cache[reference] 404 | else 405 | dict = {} 406 | @object_cache << dict 407 | length = type >> 1 408 | weak_keys = read_int8 @source # Ignore: Not supported in ruby 409 | 0.upto(length - 1) do |i| 410 | dict[amf3_deserialize] = amf3_deserialize 411 | end 412 | dict 413 | end 414 | end 415 | 416 | def amf3_read_vector vector_type 417 | type = amf3_read_integer 418 | is_reference = (type & 0x01) == 0 419 | if is_reference 420 | reference = type >> 1 421 | return @object_cache[reference] 422 | else 423 | vec = [] 424 | @object_cache << vec 425 | length = type >> 1 426 | fixed_vector = read_int8 @source # Ignore 427 | case vector_type 428 | when AMF3_VECTOR_INT_MARKER 429 | 0.upto(length - 1) do |i| 430 | int = read_word32_network(@source) 431 | int = int - 2**32 if int > MAX_INTEGER 432 | vec << int 433 | end 434 | when AMF3_VECTOR_UINT_MARKER 435 | 0.upto(length - 1) do |i| 436 | vec << read_word32_network(@source) 437 | puts vec[i].to_s(2) 438 | end 439 | when AMF3_VECTOR_DOUBLE_MARKER 440 | 0.upto(length - 1) do |i| 441 | vec << amf3_read_number 442 | end 443 | when AMF3_VECTOR_OBJECT_MARKER 444 | vector_class = amf3_read_string # Ignore 445 | puts vector_class 446 | 0.upto(length - 1) do |i| 447 | vec << amf3_deserialize 448 | end 449 | end 450 | vec 451 | end 452 | end 453 | end 454 | end 455 | end -------------------------------------------------------------------------------- /lib/rocketamf/pure/serializer.rb: -------------------------------------------------------------------------------- 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 475 | -------------------------------------------------------------------------------- /ext/rocketamf_ext/class_mapping.c: -------------------------------------------------------------------------------- 1 | #include 2 | #ifdef HAVE_RB_STR_ENCODE 3 | #include 4 | #else 5 | #include 6 | #endif 7 | #include "utility.h" 8 | 9 | extern VALUE mRocketAMF; 10 | extern VALUE mRocketAMFExt; 11 | VALUE cFastMappingSet; 12 | VALUE cTypedHash; 13 | ID id_use_ac; 14 | ID id_use_ac_ivar; 15 | ID id_mappings; 16 | ID id_mappings_ivar; 17 | ID id_hashset; 18 | 19 | typedef struct { 20 | VALUE mapset; 21 | st_table* setter_cache; 22 | st_table* prop_cache; 23 | } CLASS_MAPPING; 24 | 25 | typedef struct { 26 | st_table* as_mappings; 27 | st_table* rb_mappings; 28 | } MAPSET; 29 | 30 | /* 31 | * Mark the as_mappings and rb_mappings hashes 32 | */ 33 | static void mapset_mark(MAPSET *set) { 34 | if(!set) return; 35 | rb_mark_tbl(set->as_mappings); 36 | rb_mark_tbl(set->rb_mappings); 37 | } 38 | 39 | /* 40 | * Free the mapping tables and struct 41 | */ 42 | int mapset_free_strtable_key(st_data_t key, st_data_t value, st_data_t ignored) { 43 | xfree((void *)key); 44 | return ST_DELETE; 45 | } 46 | static void mapset_free(MAPSET *set) { 47 | st_foreach(set->as_mappings, mapset_free_strtable_key, 0); 48 | st_free_table(set->as_mappings); 49 | set->as_mappings = NULL; 50 | st_foreach(set->rb_mappings, mapset_free_strtable_key, 0); 51 | st_free_table(set->rb_mappings); 52 | set->rb_mappings = NULL; 53 | xfree(set); 54 | } 55 | 56 | /* 57 | * Allocate mapset and populate mappings with built-in mappings 58 | */ 59 | static VALUE mapset_alloc(VALUE klass) { 60 | MAPSET *set = ALLOC(MAPSET); 61 | memset(set, 0, sizeof(MAPSET)); 62 | VALUE self = Data_Wrap_Struct(klass, mapset_mark, mapset_free, set); 63 | 64 | // Initialize internal data 65 | set->as_mappings = st_init_strtable(); 66 | set->rb_mappings = st_init_strtable(); 67 | 68 | return self; 69 | } 70 | 71 | /* 72 | * call-seq: 73 | * RocketAMF::Ext::MappingSet.new 74 | * 75 | * Creates a mapping set object and populates the default mappings 76 | */ 77 | static VALUE mapset_init(VALUE self) { 78 | rb_funcall(self, rb_intern("map_defaults"), 0); 79 | return self; 80 | } 81 | 82 | /* 83 | * call-seq: 84 | * m.map_defaults 85 | * 86 | * Adds required mapping configs, calling map for the required base mappings 87 | */ 88 | static VALUE mapset_map_defaults(VALUE self) { 89 | const int NUM_MAPPINGS = 9; 90 | const char* ruby_classes[] = { 91 | "RocketAMF::Values::AbstractMessage", 92 | "RocketAMF::Values::RemotingMessage", 93 | "RocketAMF::Values::AsyncMessage", 94 | "RocketAMF::Values::AsyncMessageExt", 95 | "RocketAMF::Values::CommandMessage", 96 | "RocketAMF::Values::CommandMessageExt", 97 | "RocketAMF::Values::AcknowledgeMessage", 98 | "RocketAMF::Values::AcknowledgeMessageExt", 99 | "RocketAMF::Values::ErrorMessage" 100 | }; 101 | const char* as_classes[] = { 102 | "flex.messaging.messages.AbstractMessage", 103 | "flex.messaging.messages.RemotingMessage", 104 | "flex.messaging.messages.AsyncMessage", 105 | "DSA", 106 | "flex.messaging.messages.CommandMessage", 107 | "DSC", 108 | "flex.messaging.messages.AcknowledgeMessage", 109 | "DSK", 110 | "flex.messaging.messages.ErrorMessage" 111 | }; 112 | 113 | int i; 114 | ID map_id = rb_intern("map"); 115 | VALUE params = rb_hash_new(); 116 | VALUE as_sym = ID2SYM(rb_intern("as")); 117 | VALUE ruby_sym = ID2SYM(rb_intern("ruby")); 118 | for(i = 0; i < NUM_MAPPINGS; i++) { 119 | rb_hash_aset(params, as_sym, rb_str_new2(as_classes[i])); 120 | rb_hash_aset(params, ruby_sym, rb_str_new2(ruby_classes[i])); 121 | rb_funcall(self, map_id, 1, params); 122 | } 123 | 124 | return self; 125 | } 126 | 127 | /* 128 | * call-seq: 129 | * m.map :as => 'com.example.Date', :ruby => "Example::Date' 130 | * 131 | * Map a given AS class to a ruby class. Use fully qualified names for both. 132 | */ 133 | static VALUE mapset_map(VALUE self, VALUE mapping) { 134 | MAPSET *set; 135 | Data_Get_Struct(self, MAPSET, set); 136 | 137 | VALUE as_class = rb_hash_aref(mapping, ID2SYM(rb_intern("as"))); 138 | VALUE rb_class = rb_hash_aref(mapping, ID2SYM(rb_intern("ruby"))); 139 | st_insert(set->as_mappings, (st_data_t)strdup(RSTRING_PTR(as_class)), rb_class); 140 | st_insert(set->rb_mappings, (st_data_t)strdup(RSTRING_PTR(rb_class)), as_class); 141 | 142 | return Qnil; 143 | } 144 | 145 | /* 146 | * Internal method for looking up a given ruby class's AS class name or Qnil if 147 | * not found 148 | */ 149 | static VALUE mapset_as_lookup(VALUE self, const char* class_name) { 150 | MAPSET *set; 151 | Data_Get_Struct(self, MAPSET, set); 152 | 153 | VALUE as_name; 154 | if(st_lookup(set->rb_mappings, (st_data_t)class_name, &as_name)) { 155 | return as_name; 156 | } else { 157 | return Qnil; 158 | } 159 | } 160 | 161 | /* 162 | * Internal method for looking up a given AS class names ruby class name mapping 163 | * or Qnil if not found 164 | */ 165 | static VALUE mapset_rb_lookup(VALUE self, const char* class_name) { 166 | MAPSET *set; 167 | Data_Get_Struct(self, MAPSET, set); 168 | 169 | VALUE rb_name; 170 | if(st_lookup(set->as_mappings, (st_data_t)class_name, &rb_name)) { 171 | return rb_name; 172 | } else { 173 | return Qnil; 174 | } 175 | } 176 | 177 | /* 178 | * Mark the mapset object and property lookup cache 179 | */ 180 | static void mapping_mark(CLASS_MAPPING *map) { 181 | if(!map) return; 182 | rb_gc_mark(map->mapset); 183 | rb_mark_tbl(map->prop_cache); 184 | } 185 | 186 | /* 187 | * Free prop cache table and struct 188 | */ 189 | static void mapping_free(CLASS_MAPPING *map) { 190 | st_free_table(map->setter_cache); 191 | st_free_table(map->prop_cache); 192 | xfree(map); 193 | } 194 | 195 | /* 196 | * Allocate class mapping struct 197 | */ 198 | static VALUE mapping_alloc(VALUE klass) { 199 | CLASS_MAPPING *map = ALLOC(CLASS_MAPPING); 200 | memset(map, 0, sizeof(CLASS_MAPPING)); 201 | VALUE self = Data_Wrap_Struct(klass, mapping_mark, mapping_free, map); 202 | map->setter_cache = st_init_numtable(); 203 | map->prop_cache = st_init_numtable(); 204 | return self; 205 | } 206 | 207 | /* 208 | * Class-level getter for use_array_collection 209 | */ 210 | static VALUE mapping_s_array_collection_get(VALUE klass) { 211 | VALUE use_ac = rb_ivar_get(klass, id_use_ac_ivar); 212 | if(use_ac == Qnil) { 213 | use_ac = Qfalse; 214 | rb_ivar_set(klass, id_use_ac_ivar, use_ac); 215 | } 216 | return use_ac; 217 | } 218 | 219 | /* 220 | * Class-level setter for use_array_collection 221 | */ 222 | static VALUE mapping_s_array_collection_set(VALUE klass, VALUE use_ac) { 223 | return rb_ivar_set(klass, id_use_ac_ivar, use_ac); 224 | } 225 | 226 | /* 227 | * Return MappingSet for class mapper, creating if uninitialized 228 | */ 229 | static VALUE mapping_s_mappings(VALUE klass) { 230 | VALUE mappings = rb_ivar_get(klass, id_mappings_ivar); 231 | if(mappings == Qnil) { 232 | mappings = rb_class_new_instance(0, NULL, cFastMappingSet); 233 | rb_ivar_set(klass, id_mappings_ivar, mappings); 234 | } 235 | return mappings; 236 | } 237 | 238 | /* 239 | * call-seq: 240 | * mapper.define {|m| block } => nil 241 | * 242 | * Define class mappings in the block. Block is passed a MappingSet object as 243 | * the first parameter. See RocketAMF::ClassMapping for details. 244 | */ 245 | static VALUE mapping_s_define(VALUE klass) { 246 | if (rb_block_given_p()) { 247 | VALUE mappings = rb_funcall(klass, id_mappings, 0); 248 | rb_yield(mappings); 249 | } 250 | return Qnil; 251 | } 252 | 253 | /* 254 | * Reset class mappings 255 | */ 256 | static VALUE mapping_s_reset(VALUE klass) { 257 | rb_ivar_set(klass, id_use_ac_ivar, Qfalse); 258 | rb_ivar_set(klass, id_mappings_ivar, Qnil); 259 | return Qnil; 260 | } 261 | 262 | /* 263 | * Initialize class mapping object, setting use_class_mapping to false 264 | */ 265 | static VALUE mapping_init(VALUE self) { 266 | CLASS_MAPPING *map; 267 | Data_Get_Struct(self, CLASS_MAPPING, map); 268 | map->mapset = rb_funcall(CLASS_OF(self), id_mappings, 0); 269 | VALUE use_ac = rb_funcall(CLASS_OF(self), id_use_ac, 0); 270 | rb_ivar_set(self, id_use_ac_ivar, use_ac); 271 | return self; 272 | } 273 | 274 | /* 275 | * call-seq: 276 | * mapper.get_as_class_name => str 277 | * 278 | * Returns the AS class name for the given ruby object. Will also take a string 279 | * containing the ruby class name. 280 | */ 281 | static VALUE mapping_as_class_name(VALUE self, VALUE obj) { 282 | CLASS_MAPPING *map; 283 | Data_Get_Struct(self, CLASS_MAPPING, map); 284 | 285 | int type = TYPE(obj); 286 | const char* class_name; 287 | if(type == T_STRING) { 288 | // Use strings as the class name 289 | class_name = RSTRING_PTR(obj); 290 | } else { 291 | // Look up the class name and use that 292 | VALUE klass = CLASS_OF(obj); 293 | class_name = rb_class2name(klass); 294 | if(klass == cTypedHash) { 295 | VALUE orig_name = rb_funcall(obj, rb_intern("type"), 0); 296 | class_name = RSTRING_PTR(orig_name); 297 | } else if(type == T_HASH) { 298 | // Don't bother looking up hash mapping, but need to check class name first in case it's a typed hash 299 | return Qnil; 300 | } 301 | } 302 | 303 | return mapset_as_lookup(map->mapset, class_name); 304 | } 305 | 306 | /* 307 | * call_seq: 308 | * mapper.get_ruby_obj => obj 309 | * 310 | * Instantiates a ruby object using the mapping configuration based on the 311 | * source AS class name. If there is no mapping defined, it returns a 312 | * RocketAMF::Values::TypedHash with the serialized class name. 313 | */ 314 | static VALUE mapping_get_ruby_obj(VALUE self, VALUE name) { 315 | CLASS_MAPPING *map; 316 | Data_Get_Struct(self, CLASS_MAPPING, map); 317 | 318 | VALUE argv[1]; 319 | VALUE ruby_class_name = mapset_rb_lookup(map->mapset, RSTRING_PTR(name)); 320 | if(ruby_class_name == Qnil) { 321 | argv[0] = name; 322 | return rb_class_new_instance(1, argv, cTypedHash); 323 | } else { 324 | VALUE base_const = rb_mKernel; 325 | char* endptr; 326 | char* ptr = RSTRING_PTR(ruby_class_name); 327 | while((endptr = strstr(ptr,"::"))) { 328 | endptr[0] = '\0'; // NULL terminate to make string ops work 329 | base_const = rb_const_get(base_const, rb_intern(ptr)); 330 | endptr[0] = ':'; // Restore correct char 331 | ptr = endptr + 2; 332 | } 333 | return rb_class_new_instance(0, NULL, rb_const_get(base_const, rb_intern(ptr))); 334 | } 335 | } 336 | 337 | /* 338 | * st_table iterator for populating a given object from a property hash 339 | */ 340 | static int mapping_populate_iter(VALUE key, VALUE val, const VALUE args[2]) { 341 | CLASS_MAPPING *map; 342 | Data_Get_Struct(args[0], CLASS_MAPPING, map); 343 | VALUE obj = args[1]; 344 | 345 | if(TYPE(obj) == T_HASH) { 346 | rb_hash_aset(obj, key, val); 347 | return ST_CONTINUE; 348 | } 349 | 350 | if(TYPE(key) != T_SYMBOL) rb_raise(rb_eArgError, "Invalid type for property key: %d", TYPE(key)); 351 | 352 | // Calculate symbol for setter function 353 | ID key_id = SYM2ID(key); 354 | ID setter_id; 355 | if(!st_lookup(map->setter_cache, key_id, &setter_id)) { 356 | // Calculate symbol 357 | const char* key_str = rb_id2name(key_id); 358 | long len = strlen(key_str); 359 | char* setter = ALLOC_N(char, len+2); 360 | memcpy(setter, key_str, len); 361 | setter[len] = '='; 362 | setter[len+1] = '\0'; 363 | setter_id = rb_intern(setter); 364 | xfree(setter); 365 | 366 | // Store it 367 | st_add_direct(map->setter_cache, key_id, setter_id); 368 | } 369 | 370 | if(rb_respond_to(obj, setter_id)) { 371 | rb_funcall(obj, setter_id, 1, val); 372 | } else if(rb_respond_to(obj, id_hashset)) { 373 | rb_funcall(obj, id_hashset, 2, key, val); 374 | } 375 | 376 | return ST_CONTINUE; 377 | } 378 | 379 | /* 380 | * call-seq: 381 | * mapper.populate_ruby_obj(obj, props, dynamic_props=nil) => obj 382 | * 383 | * Populates the ruby object using the given properties. Property hashes MUST 384 | * have symbol keys, or it will raise an exception. 385 | */ 386 | static VALUE mapping_populate(int argc, VALUE *argv, VALUE self) { 387 | // Check args 388 | VALUE obj, props, dynamic_props; 389 | rb_scan_args(argc, argv, "21", &obj, &props, &dynamic_props); 390 | 391 | VALUE args[2] = {self, obj}; 392 | st_foreach(RHASH_TBL(props), mapping_populate_iter, (st_data_t)args); 393 | if(dynamic_props != Qnil) { 394 | st_foreach(RHASH_TBL(dynamic_props), mapping_populate_iter, (st_data_t)args); 395 | } 396 | 397 | return obj; 398 | } 399 | 400 | /* 401 | * call-seq: 402 | * mapper.props_for_serialization(obj) => hash 403 | * 404 | * Extracts all exportable properties from the given ruby object and returns 405 | * them in a hash. For performance purposes, property detection is only performed 406 | * once for a given class instance, and then cached for all instances of that 407 | * class. IF YOU'RE ADDING AND REMOVING PROPERTIES FROM CLASS INSTANCES YOU 408 | * CANNOT USE THE FAST CLASS MAPPER. 409 | */ 410 | static VALUE mapping_props(VALUE self, VALUE obj) { 411 | CLASS_MAPPING *map; 412 | Data_Get_Struct(self, CLASS_MAPPING, map); 413 | 414 | if(TYPE(obj) == T_HASH) { 415 | return obj; 416 | } 417 | 418 | // Get "properties" 419 | VALUE props_ary; 420 | VALUE klass = CLASS_OF(obj); 421 | long i, len; 422 | if(!st_lookup(map->prop_cache, klass, &props_ary)) { 423 | props_ary = rb_ary_new(); 424 | 425 | // Build props array 426 | VALUE all_methods = rb_class_public_instance_methods(0, NULL, klass); 427 | VALUE object_methods = rb_class_public_instance_methods(0, NULL, rb_cObject); 428 | VALUE possible_methods = rb_funcall(all_methods, rb_intern("-"), 1, object_methods); 429 | len = RARRAY_LEN(possible_methods); 430 | for(i = 0; i < len; i++) { 431 | VALUE meth = rb_obj_method(obj, RARRAY_PTR(possible_methods)[i]); 432 | VALUE arity = rb_funcall(meth, rb_intern("arity"), 0); 433 | if(FIX2INT(arity) == 0) { 434 | rb_ary_push(props_ary, RARRAY_PTR(possible_methods)[i]); 435 | } 436 | } 437 | 438 | // Store it 439 | st_add_direct(map->prop_cache, klass, props_ary); 440 | } 441 | 442 | // Build properties hash using list of properties 443 | VALUE props = rb_hash_new(); 444 | len = RARRAY_LEN(props_ary); 445 | for(i = 0; i < len; i++) { 446 | VALUE key = RARRAY_PTR(props_ary)[i]; 447 | ID getter = (TYPE(key) == T_STRING) ? rb_intern(RSTRING_PTR(key)) : SYM2ID(key); 448 | rb_hash_aset(props, key, rb_funcall(obj, getter, 0)); 449 | } 450 | 451 | return props; 452 | } 453 | 454 | void Init_rocket_amf_fast_class_mapping() { 455 | // Define map set 456 | cFastMappingSet = rb_define_class_under(mRocketAMFExt, "FastMappingSet", rb_cObject); 457 | rb_define_alloc_func(cFastMappingSet, mapset_alloc); 458 | rb_define_method(cFastMappingSet, "initialize", mapset_init, 0); 459 | rb_define_method(cFastMappingSet, "map_defaults", mapset_map_defaults, 0); 460 | rb_define_method(cFastMappingSet, "map", mapset_map, 1); 461 | 462 | // Define FastClassMapping 463 | VALUE cFastClassMapping = rb_define_class_under(mRocketAMFExt, "FastClassMapping", rb_cObject); 464 | rb_define_alloc_func(cFastClassMapping, mapping_alloc); 465 | rb_define_singleton_method(cFastClassMapping, "use_array_collection", mapping_s_array_collection_get, 0); 466 | rb_define_singleton_method(cFastClassMapping, "use_array_collection=", mapping_s_array_collection_set, 1); 467 | rb_define_singleton_method(cFastClassMapping, "mappings", mapping_s_mappings, 0); 468 | rb_define_singleton_method(cFastClassMapping, "reset", mapping_s_reset, 0); 469 | rb_define_singleton_method(cFastClassMapping, "define", mapping_s_define, 0); 470 | rb_define_attr(cFastClassMapping, "use_array_collection", 1, 0); 471 | rb_define_method(cFastClassMapping, "initialize", mapping_init, 0); 472 | rb_define_method(cFastClassMapping, "get_as_class_name", mapping_as_class_name, 1); 473 | rb_define_method(cFastClassMapping, "get_ruby_obj", mapping_get_ruby_obj, 1); 474 | rb_define_method(cFastClassMapping, "populate_ruby_obj", mapping_populate, -1); 475 | rb_define_method(cFastClassMapping, "props_for_serialization", mapping_props, 1); 476 | 477 | // Cache values 478 | cTypedHash = rb_const_get(rb_const_get(mRocketAMF, rb_intern("Values")), rb_intern("TypedHash")); 479 | id_use_ac = rb_intern("use_array_collection"); 480 | id_use_ac_ivar = rb_intern("@use_array_collection"); 481 | id_mappings = rb_intern("mappings"); 482 | id_mappings_ivar = rb_intern("@mappings"); 483 | id_hashset = rb_intern("[]="); 484 | } -------------------------------------------------------------------------------- /spec/deserializer_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require "spec_helper.rb" 4 | 5 | describe "when deserializing" do 6 | before :each do 7 | RocketAMF::ClassMapper.reset 8 | end 9 | 10 | it "should raise exception with invalid version number" do 11 | lambda { 12 | RocketAMF.deserialize("", 5) 13 | }.should raise_error("unsupported version 5") 14 | end 15 | 16 | describe "AMF0" do 17 | it "should update source pos if source is a StringIO object" do 18 | input = StringIO.new(object_fixture('amf0-number.bin')) 19 | input.pos.should == 0 20 | output = RocketAMF.deserialize(input, 0) 21 | input.pos.should == 9 22 | end 23 | 24 | it "should deserialize numbers" do 25 | input = object_fixture('amf0-number.bin') 26 | output = RocketAMF.deserialize(input, 0) 27 | output.should == 3.5 28 | end 29 | 30 | it "should deserialize booleans" do 31 | input = object_fixture('amf0-boolean.bin') 32 | output = RocketAMF.deserialize(input, 0) 33 | output.should === true 34 | end 35 | 36 | it "should deserialize UTF8 strings" do 37 | input = object_fixture('amf0-string.bin') 38 | output = RocketAMF.deserialize(input, 0) 39 | output.should == "this is a テスト" 40 | end 41 | 42 | it "should deserialize nulls" do 43 | input = object_fixture('amf0-null.bin') 44 | output = RocketAMF.deserialize(input, 0) 45 | output.should == nil 46 | end 47 | 48 | it "should deserialize undefineds" do 49 | input = object_fixture('amf0-undefined.bin') 50 | output = RocketAMF.deserialize(input, 0) 51 | output.should == nil 52 | end 53 | 54 | it "should deserialize hashes" do 55 | input = object_fixture('amf0-hash.bin') 56 | output = RocketAMF.deserialize(input, 0) 57 | output.should == {'a' => 'b', 'c' => 'd'} 58 | end 59 | 60 | it "should deserialize hashes with empty string keys" do 61 | input = object_fixture('amf0-empty-string-key-hash.bin') 62 | output = RocketAMF.deserialize(input, 0) 63 | output.should == {'a' => 'b', 'c' => 'd', '' => 'last'} 64 | end 65 | 66 | it "should deserialize arrays from flash player" do 67 | # Even Array is serialized as a "hash" 68 | input = object_fixture('amf0-ecma-ordinal-array.bin') 69 | output = RocketAMF.deserialize(input, 0) 70 | output.should == {'0' => 'a', '1' => 'b', '2' => 'c', '3' => 'd'} 71 | end 72 | 73 | it "should deserialize strict arrays" do 74 | input = object_fixture('amf0-strict-array.bin') 75 | output = RocketAMF.deserialize(input, 0) 76 | output.should == ['a', 'b', 'c', 'd'] 77 | end 78 | 79 | it "should deserialize dates" do 80 | input = object_fixture('amf0-time.bin') 81 | output = RocketAMF.deserialize(input, 0) 82 | output.should == Time.utc(2003, 2, 13, 5) 83 | end 84 | 85 | it "should deserialize an XML document" do 86 | input = object_fixture('amf0-xml-doc.bin') 87 | output = RocketAMF.deserialize(input, 0) 88 | output.should == '' 89 | end 90 | 91 | it "should deserialize anonymous objects" do 92 | input = object_fixture('amf0-object.bin') 93 | output = RocketAMF.deserialize(input, 0) 94 | output.should == {'foo' => 'baz', 'bar' => 3.14} 95 | output.type.should == "" 96 | end 97 | 98 | it "should deserialize an unmapped object as a dynamic anonymous object" do 99 | input = object_fixture("amf0-typed-object.bin") 100 | output = RocketAMF.deserialize(input, 0) 101 | 102 | output.type.should == 'org.amf.ASClass' 103 | output.should == {'foo' => 'bar', 'baz' => nil} 104 | end 105 | 106 | it "should deserialize a mapped object as a mapped ruby class instance" do 107 | RocketAMF::ClassMapper.define {|m| m.map :as => 'org.amf.ASClass', :ruby => 'RubyClass'} 108 | 109 | input = object_fixture("amf0-typed-object.bin") 110 | output = RocketAMF.deserialize(input, 0) 111 | 112 | output.should be_a(RubyClass) 113 | output.foo.should == 'bar' 114 | output.baz.should == nil 115 | end 116 | 117 | it "should deserialize references properly" do 118 | input = object_fixture('amf0-ref-test.bin') 119 | output = RocketAMF.deserialize(input, 0) 120 | output.length.should == 2 121 | output["0"].should === output["1"] 122 | end 123 | end 124 | 125 | describe "AMF3" do 126 | it "should update source pos if source is a StringIO object" do 127 | input = StringIO.new(object_fixture('amf3-null.bin')) 128 | input.pos.should == 0 129 | output = RocketAMF.deserialize(input, 3) 130 | input.pos.should == 1 131 | end 132 | 133 | describe "simple messages" do 134 | it "should deserialize a null" do 135 | input = object_fixture("amf3-null.bin") 136 | output = RocketAMF.deserialize(input, 3) 137 | output.should == nil 138 | end 139 | 140 | it "should deserialize a false" do 141 | input = object_fixture("amf3-false.bin") 142 | output = RocketAMF.deserialize(input, 3) 143 | output.should == false 144 | end 145 | 146 | it "should deserialize a true" do 147 | input = object_fixture("amf3-true.bin") 148 | output = RocketAMF.deserialize(input, 3) 149 | output.should == true 150 | end 151 | 152 | it "should deserialize integers" do 153 | input = object_fixture("amf3-max.bin") 154 | output = RocketAMF.deserialize(input, 3) 155 | output.should == RocketAMF::MAX_INTEGER 156 | 157 | input = object_fixture("amf3-0.bin") 158 | output = RocketAMF.deserialize(input, 3) 159 | output.should == 0 160 | 161 | input = object_fixture("amf3-min.bin") 162 | output = RocketAMF.deserialize(input, 3) 163 | output.should == RocketAMF::MIN_INTEGER 164 | end 165 | 166 | it "should deserialize large integers" do 167 | input = object_fixture("amf3-large-max.bin") 168 | output = RocketAMF.deserialize(input, 3) 169 | output.should == RocketAMF::MAX_INTEGER + 1 170 | 171 | input = object_fixture("amf3-large-min.bin") 172 | output = RocketAMF.deserialize(input, 3) 173 | output.should == RocketAMF::MIN_INTEGER - 1 174 | end 175 | 176 | it "should deserialize BigNums" do 177 | input = object_fixture("amf3-bignum.bin") 178 | output = RocketAMF.deserialize(input, 3) 179 | output.should == 2**1000 180 | end 181 | 182 | it "should deserialize a simple string" do 183 | input = object_fixture("amf3-string.bin") 184 | output = RocketAMF.deserialize(input, 3) 185 | output.should == "String . String" 186 | end 187 | 188 | it "should deserialize a symbol as a string" do 189 | input = object_fixture("amf3-symbol.bin") 190 | output = RocketAMF.deserialize(input, 3) 191 | output.should == "foo" 192 | end 193 | 194 | it "should deserialize dates" do 195 | input = object_fixture("amf3-date.bin") 196 | output = RocketAMF.deserialize(input, 3) 197 | output.should == Time.at(0) 198 | end 199 | 200 | it "should deserialize XML" do 201 | # XMLDocument tag 202 | input = object_fixture("amf3-xml-doc.bin") 203 | output = RocketAMF.deserialize(input, 3) 204 | output.should == '' 205 | 206 | # XML tag 207 | input = object_fixture("amf3-xml.bin") 208 | output = RocketAMF.deserialize(input, 3) 209 | output.should == '' 210 | end 211 | end 212 | 213 | describe "objects" do 214 | it "should deserialize an unmapped object as a dynamic anonymous object" do 215 | input = object_fixture("amf3-dynamic-object.bin") 216 | output = RocketAMF.deserialize(input, 3) 217 | 218 | expected = { 219 | 'property_one' => 'foo', 220 | 'nil_property' => nil, 221 | 'another_public_property' => 'a_public_value' 222 | } 223 | output.should == expected 224 | output.type.should == "" 225 | end 226 | 227 | it "should deserialize a mapped object as a mapped ruby class instance" do 228 | RocketAMF::ClassMapper.define {|m| m.map :as => 'org.amf.ASClass', :ruby => 'RubyClass'} 229 | 230 | input = object_fixture("amf3-typed-object.bin") 231 | output = RocketAMF.deserialize(input, 3) 232 | 233 | output.should be_a(RubyClass) 234 | output.foo.should == 'bar' 235 | output.baz.should == nil 236 | end 237 | 238 | it "should deserialize externalizable objects" do 239 | RocketAMF::ClassMapper.define {|m| m.map :as => 'ExternalizableTest', :ruby => 'ExternalizableTest'} 240 | 241 | input = object_fixture("amf3-externalizable.bin") 242 | output = RocketAMF.deserialize(input, 3) 243 | 244 | output.length.should == 2 245 | output[0].one.should == 5 246 | output[1].two.should == 5 247 | end 248 | 249 | it "should deserialize a hash as a dynamic anonymous object" do 250 | input = object_fixture("amf3-hash.bin") 251 | output = RocketAMF.deserialize(input, 3) 252 | output.should == {'foo' => "bar", 'answer' => 42} 253 | end 254 | 255 | it "should deserialize an empty array" do 256 | input = object_fixture("amf3-empty-array.bin") 257 | output = RocketAMF.deserialize(input, 3) 258 | output.should == [] 259 | end 260 | 261 | it "should deserialize an array of primitives" do 262 | input = object_fixture("amf3-primitive-array.bin") 263 | output = RocketAMF.deserialize(input, 3) 264 | output.should == [1,2,3,4,5] 265 | end 266 | 267 | it "should deserialize an associative array" do 268 | input = object_fixture("amf3-associative-array.bin") 269 | output = RocketAMF.deserialize(input, 3) 270 | output.should == {0=>"bar1", 1=>"bar2", 2=>"bar3", "asdf"=>"fdsa", "foo"=>"bar", "42"=>"bar"} 271 | end 272 | 273 | it "should deserialize an array of mixed objects" do 274 | input = object_fixture("amf3-mixed-array.bin") 275 | output = RocketAMF.deserialize(input, 3) 276 | 277 | h1 = {'foo_one' => "bar_one"} 278 | h2 = {'foo_two' => ""} 279 | so1 = {'foo_three' => 42} 280 | output.should == [h1, h2, so1, {}, [h1, h2, so1], [], 42, "", [], "", {}, "bar_one", so1] 281 | end 282 | 283 | it "should deserialize an array collection as an array" do 284 | input = object_fixture("amf3-array-collection.bin") 285 | output = RocketAMF.deserialize(input, 3) 286 | 287 | output.class.should == Array 288 | output.should == ["foo", "bar"] 289 | end 290 | 291 | it "should deserialize a complex set of array collections" do 292 | RocketAMF::ClassMapper.define {|m| m.map :as => 'org.amf.ASClass', :ruby => 'RubyClass'} 293 | input = object_fixture('amf3-complex-array-collection.bin') 294 | 295 | output = RocketAMF.deserialize(input, 3) 296 | 297 | output[0].should == ["foo", "bar"] 298 | output[1][0].should be_a(RubyClass) 299 | output[1][1].should be_a(RubyClass) 300 | output[2].should === output[1] 301 | end 302 | 303 | it "should deserialize a byte array" do 304 | input = object_fixture("amf3-byte-array.bin") 305 | output = RocketAMF.deserialize(input, 3) 306 | 307 | output.should be_a(StringIO) 308 | expected = "\000\003これtest\100" 309 | expected.force_encoding("ASCII-8BIT") if expected.respond_to?(:force_encoding) 310 | output.string.should == expected 311 | end 312 | 313 | it "should deserialize an empty dictionary" do 314 | input = object_fixture("amf3-empty-dictionary.bin") 315 | output = RocketAMF.deserialize(input, 3) 316 | output.should == {} 317 | end 318 | 319 | it "should deserialize a dictionary" do 320 | input = object_fixture("amf3-dictionary.bin") 321 | output = RocketAMF.deserialize(input, 3) 322 | 323 | keys = output.keys 324 | keys.length.should == 2 325 | obj_key, str_key = keys[0].is_a?(RocketAMF::Values::TypedHash) ? [keys[0], keys[1]] : [keys[1], keys[0]] 326 | obj_key.type.should == 'org.amf.ASClass' 327 | output[obj_key].should == "asdf2" 328 | str_key.should == "bar" 329 | output[str_key].should == "asdf1" 330 | end 331 | 332 | it "should deserialize Vector." do 333 | input = object_fixture('amf3-vector-int.bin') 334 | output = RocketAMF.deserialize(input, 3) 335 | output.should == [4, -20, 12] 336 | end 337 | 338 | it "should deserialize Vector." do 339 | input = object_fixture('amf3-vector-uint.bin') 340 | output = RocketAMF.deserialize(input, 3) 341 | output.should == [4, 20, 12] 342 | end 343 | 344 | it "should deserialize Vector." do 345 | input = object_fixture('amf3-vector-double.bin') 346 | output = RocketAMF.deserialize(input, 3) 347 | output.should == [4.3, -20.6] 348 | end 349 | 350 | it "should deserialize Vector." do 351 | input = object_fixture('amf3-vector-object.bin') 352 | output = RocketAMF.deserialize(input, 3) 353 | output[0]['foo'].should == 'foo' 354 | output[1].type.should == 'org.amf.ASClass' 355 | output[2]['foo'].should == 'baz' 356 | end 357 | end 358 | 359 | describe "and implementing the AMF Spec" do 360 | it "should keep references of duplicate strings" do 361 | input = object_fixture("amf3-string-ref.bin") 362 | output = RocketAMF.deserialize(input, 3) 363 | 364 | foo = "foo" 365 | bar = "str" 366 | output.should == [foo, bar, foo, bar, foo, {'str' => "foo"}] 367 | end 368 | 369 | it "should not reference the empty string" do 370 | input = object_fixture("amf3-empty-string-ref.bin") 371 | output = RocketAMF.deserialize(input, 3) 372 | output.should == ["",""] 373 | end 374 | 375 | it "should keep references of duplicate dates" do 376 | input = object_fixture("amf3-date-ref.bin") 377 | output = RocketAMF.deserialize(input, 3) 378 | 379 | output[0].should == Time.at(0) 380 | output[0].should equal(output[1]) 381 | # Expected object: 382 | # [DateTime.parse "1/1/1970", DateTime.parse "1/1/1970"] 383 | end 384 | 385 | it "should keep reference of duplicate objects" do 386 | input = object_fixture("amf3-object-ref.bin") 387 | output = RocketAMF.deserialize(input, 3) 388 | 389 | obj1 = {'foo' => "bar"} 390 | obj2 = {'foo' => obj1['foo']} 391 | output.should == [[obj1, obj2], "bar", [obj1, obj2]] 392 | end 393 | 394 | it "should keep reference of duplicate object traits" do 395 | RocketAMF::ClassMapper.define {|m| m.map :as => 'org.amf.ASClass', :ruby => 'RubyClass'} 396 | 397 | input = object_fixture("amf3-trait-ref.bin") 398 | output = RocketAMF.deserialize(input, 3) 399 | 400 | output[0].foo.should == "foo" 401 | output[1].foo.should == "bar" 402 | end 403 | 404 | it "should keep references of duplicate arrays" do 405 | input = object_fixture("amf3-array-ref.bin") 406 | output = RocketAMF.deserialize(input, 3) 407 | 408 | a = [1,2,3] 409 | b = %w{ a b c } 410 | output.should == [a, b, a, b] 411 | end 412 | 413 | it "should not keep references of duplicate empty arrays unless the object_id matches" do 414 | input = object_fixture("amf3-empty-array-ref.bin") 415 | output = RocketAMF.deserialize(input, 3) 416 | 417 | a = [] 418 | b = [] 419 | output.should == [a,b,a,b] 420 | end 421 | 422 | it "should keep references of duplicate XML and XMLDocuments" do 423 | input = object_fixture("amf3-xml-ref.bin") 424 | output = RocketAMF.deserialize(input, 3) 425 | output.should == ['', ''] 426 | end 427 | 428 | it "should keep references of duplicate byte arrays" do 429 | input = object_fixture("amf3-byte-array-ref.bin") 430 | output = RocketAMF.deserialize(input, 3) 431 | output[0].object_id.should == output[1].object_id 432 | output[0].string.should == "ASDF" 433 | end 434 | 435 | it "should deserialize a deep object graph with circular references" do 436 | input = object_fixture("amf3-graph-member.bin") 437 | output = RocketAMF.deserialize(input, 3) 438 | 439 | output['children'][0]['parent'].should === output 440 | output['parent'].should === nil 441 | output['children'].length.should == 2 442 | # Expected object: 443 | # parent = Hash.new 444 | # child1 = Hash.new 445 | # child1[:parent] = parent 446 | # child1[:children] = [] 447 | # child2 = Hash.new 448 | # child2[:parent] = parent 449 | # child2[:children] = [] 450 | # parent[:parent] = nil 451 | # parent[:children] = [child1, child2] 452 | end 453 | end 454 | end 455 | end -------------------------------------------------------------------------------- /spec/serializer_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require "spec_helper.rb" 4 | require 'rexml/document' 5 | require 'bigdecimal' 6 | require 'rational' 7 | 8 | describe "when serializing" do 9 | before :each do 10 | RocketAMF::ClassMapper.reset 11 | end 12 | 13 | it "should raise exception with invalid version number" do 14 | lambda { 15 | RocketAMF.serialize("", 5) 16 | }.should raise_error("unsupported version 5") 17 | end 18 | 19 | describe "AMF0" do 20 | it "should serialize nils" do 21 | output = RocketAMF.serialize(nil, 0) 22 | output.should == object_fixture('amf0-null.bin') 23 | end 24 | 25 | it "should serialize booleans" do 26 | output = RocketAMF.serialize(true, 0) 27 | output.should === object_fixture('amf0-boolean.bin') 28 | end 29 | 30 | it "should serialize numbers" do 31 | output = RocketAMF.serialize(3.5, 0) 32 | output.should == object_fixture('amf0-number.bin') 33 | end 34 | 35 | it "should serialize Numeric conformers" do 36 | output = RocketAMF.serialize(BigDecimal.new("3.5"), 0) 37 | output.should == object_fixture('amf0-number.bin') 38 | end 39 | 40 | it "should serialize strings" do 41 | output = RocketAMF.serialize("this is a テスト", 0) 42 | output.should == object_fixture('amf0-string.bin') 43 | end 44 | 45 | it "should serialize frozen strings" do 46 | output = RocketAMF.serialize("this is a テスト".freeze, 0) 47 | output.should == object_fixture('amf0-string.bin') 48 | end 49 | 50 | it "should serialize arrays" do 51 | output = RocketAMF.serialize(['a', 'b', 'c', 'd'], 0) 52 | output.should == object_fixture('amf0-strict-array.bin') 53 | end 54 | 55 | it "should serialize references" do 56 | obj = OtherClass.new 57 | obj.foo = "baz" 58 | obj.bar = 3.14 59 | 60 | output = RocketAMF.serialize({'0' => obj, '1' => obj}, 0) 61 | output.should == object_fixture('amf0-ref-test.bin') 62 | end 63 | 64 | it "should serialize Time objects" do 65 | output = RocketAMF.serialize(Time.utc(2003, 2, 13, 5), 0) 66 | output.bytesize.should == 11 67 | output[0,9].should == object_fixture('amf0-time.bin')[0,9] # Ignore TZ 68 | end 69 | 70 | it "should serialize Date objects" do 71 | output = RocketAMF.serialize(Date.civil(2020, 5, 30), 0) 72 | output.bytesize.should == 11 73 | output[0,9].should == object_fixture('amf0-date.bin')[0,9] # Ignore TZ 74 | end 75 | 76 | it "should serialize DateTime objects" do 77 | output = RocketAMF.serialize(DateTime.civil(2003, 2, 13, 5), 0) 78 | output.bytesize.should == 11 79 | output[0,9].should == object_fixture('amf0-time.bin')[0,9] # Ignore TZ 80 | end 81 | 82 | it "should serialize hashes as objects" do 83 | output = RocketAMF.serialize({:baz => nil, "foo" => "bar"}, 0) 84 | output.should == object_fixture('amf0-untyped-object.bin') 85 | end 86 | 87 | it "should serialize unmapped objects" do 88 | obj = RubyClass.new 89 | obj.foo = "bar" 90 | 91 | output = RocketAMF.serialize(obj, 0) 92 | output.should == object_fixture('amf0-untyped-object.bin') 93 | end 94 | 95 | it "should serialize mapped objects" do 96 | obj = RubyClass.new 97 | obj.foo = "bar" 98 | RocketAMF::ClassMapper.define {|m| m.map :as => 'org.amf.ASClass', :ruby => 'RubyClass'} 99 | 100 | output = RocketAMF.serialize(obj, 0) 101 | output.should == object_fixture('amf0-typed-object.bin') 102 | end 103 | 104 | describe "and handling encodings", :if => "".respond_to?(:force_encoding) do 105 | it "should support multiple encodings" do 106 | shift_str = "\x53\x68\x69\x66\x74\x20\x83\x65\x83\x58\x83\x67".force_encoding("Shift_JIS") # "Shift テスト" 107 | utf_str = "\x55\x54\x46\x20\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88".force_encoding("UTF-8") # "UTF テスト" 108 | output = RocketAMF.serialize({:shift => shift_str, :utf => utf_str, :zed => 5}, 0) 109 | output.should == object_fixture("amf0-complex-encoded-string.bin") 110 | end 111 | end 112 | end 113 | 114 | describe "AMF3" do 115 | describe "simple messages" do 116 | it "should serialize a null" do 117 | expected = object_fixture("amf3-null.bin") 118 | output = RocketAMF.serialize(nil, 3) 119 | output.should == expected 120 | end 121 | 122 | it "should serialize a false" do 123 | expected = object_fixture("amf3-false.bin") 124 | output = RocketAMF.serialize(false, 3) 125 | output.should == expected 126 | end 127 | 128 | it "should serialize a true" do 129 | expected = object_fixture("amf3-true.bin") 130 | output = RocketAMF.serialize(true, 3) 131 | output.should == expected 132 | end 133 | 134 | it "should serialize integers" do 135 | expected = object_fixture("amf3-max.bin") 136 | input = RocketAMF::MAX_INTEGER 137 | output = RocketAMF.serialize(input, 3) 138 | output.should == expected 139 | 140 | expected = object_fixture("amf3-0.bin") 141 | output = RocketAMF.serialize(0, 3) 142 | output.should == expected 143 | 144 | expected = object_fixture("amf3-min.bin") 145 | input = RocketAMF::MIN_INTEGER 146 | output = RocketAMF.serialize(input, 3) 147 | output.should == expected 148 | end 149 | 150 | it "should serialize large integers" do 151 | expected = object_fixture("amf3-large-max.bin") 152 | input = RocketAMF::MAX_INTEGER + 1 153 | output = RocketAMF.serialize(input, 3) 154 | output.should == expected 155 | 156 | expected = object_fixture("amf3-large-min.bin") 157 | input = RocketAMF::MIN_INTEGER - 1 158 | output = RocketAMF.serialize(input, 3) 159 | output.should == expected 160 | end 161 | 162 | it "should serialize floats" do 163 | expected = object_fixture("amf3-float.bin") 164 | input = 3.5 165 | output = RocketAMF.serialize(input, 3) 166 | output.should == expected 167 | end 168 | 169 | it "should serialize BigNums" do 170 | expected = object_fixture("amf3-bigNum.bin") 171 | input = 2**1000 172 | output = RocketAMF.serialize(input, 3) 173 | output.should == expected 174 | end 175 | 176 | it "should serialize float Numeric conformers" do 177 | expected = object_fixture("amf3-float.bin") 178 | input = Rational(7, 2) # 3.5 179 | output = RocketAMF.serialize(input, 3) 180 | output.should == expected 181 | end 182 | 183 | it "should serialize a simple string" do 184 | expected = object_fixture("amf3-string.bin") 185 | input = "String . String" 186 | output = RocketAMF.serialize(input, 3) 187 | output.should == expected 188 | end 189 | 190 | it "should serialize a frozen string" do 191 | expected = object_fixture("amf3-string.bin") 192 | input = "String . String".freeze 193 | output = RocketAMF.serialize(input, 3) 194 | output.should == expected 195 | end 196 | 197 | it "should serialize a symbol as a string" do 198 | expected = object_fixture("amf3-symbol.bin") 199 | output = RocketAMF.serialize(:foo, 3) 200 | output.should == expected 201 | end 202 | 203 | it "should serialize Time objects" do 204 | expected = object_fixture("amf3-date.bin") 205 | input = Time.utc 1970, 1, 1, 0 206 | output = RocketAMF.serialize(input, 3) 207 | output.should == expected 208 | end 209 | 210 | it "should serialize Date objects" do 211 | expected = object_fixture("amf3-date.bin") 212 | input = Date.civil 1970, 1, 1, 0 213 | output = RocketAMF.serialize(input, 3) 214 | output.should == expected 215 | end 216 | 217 | it "should serialize DateTime objects" do 218 | expected = object_fixture("amf3-date.bin") 219 | input = DateTime.civil 1970, 1, 1, 0 220 | output = RocketAMF.serialize(input, 3) 221 | output.should == expected 222 | end 223 | end 224 | 225 | describe "objects" do 226 | it "should serialize an unmapped object as a dynamic anonymous object" do 227 | class NonMappedObject 228 | def another_public_property 229 | 'a_public_value' 230 | end 231 | 232 | attr_accessor :nil_property 233 | attr_accessor :property_one 234 | attr_writer :read_only_prop 235 | 236 | def method_with_arg arg='foo' 237 | arg 238 | end 239 | end 240 | obj = NonMappedObject.new 241 | obj.property_one = 'foo' 242 | obj.nil_property = nil 243 | 244 | expected = object_fixture("amf3-dynamic-object.bin") 245 | input = obj 246 | output = RocketAMF.serialize(input, 3) 247 | output.should == expected 248 | end 249 | 250 | it "should serialize externalizable objects" do 251 | a = ExternalizableTest.new 252 | a.one = 5 253 | a.two = 7 254 | b = ExternalizableTest.new 255 | b.one = 13 256 | b.two = 5 257 | obj = [a, b] 258 | 259 | expected = object_fixture("amf3-externalizable.bin") 260 | input = obj 261 | output = RocketAMF.serialize(input, 3) 262 | output.should == expected 263 | end 264 | 265 | it "should serialize a hash as a dynamic anonymous object" do 266 | hash = {} 267 | hash[:answer] = 42 268 | hash['foo'] = "bar" 269 | 270 | expected = object_fixture("amf3-hash.bin") 271 | input = hash 272 | output = RocketAMF.serialize(input, 3) 273 | output.should == expected 274 | end 275 | 276 | it "should serialize an empty array" do 277 | expected = object_fixture("amf3-empty-array.bin") 278 | input = [] 279 | output = RocketAMF.serialize(input, 3) 280 | output.should == expected 281 | end 282 | 283 | it "should serialize an array of primatives" do 284 | expected = object_fixture("amf3-primitive-array.bin") 285 | input = [1, 2, 3, 4, 5] 286 | output = RocketAMF.serialize(input, 3) 287 | output.should == expected 288 | end 289 | 290 | it "should serialize an array of mixed objects" do 291 | h1 = {:foo_one => "bar_one"} 292 | h2 = {:foo_two => ""} 293 | class SimpleObj 294 | attr_accessor :foo_three 295 | end 296 | so1 = SimpleObj.new 297 | so1.foo_three = 42 298 | 299 | expected = object_fixture("amf3-mixed-array.bin") 300 | input = [h1, h2, so1, {}, [h1, h2, so1], [], 42, "", [], "", {}, "bar_one", so1] 301 | output = RocketAMF.serialize(input, 3) 302 | output.should == expected 303 | end 304 | 305 | it "should serialize an array as an array collection" do 306 | expected = object_fixture('amf3-array-collection.bin') 307 | 308 | # Test global 309 | RocketAMF::ClassMapper.use_array_collection = true 310 | input = ["foo", "bar"] 311 | output = RocketAMF.serialize(input, 3) 312 | output.should == expected 313 | RocketAMF::ClassMapper.use_array_collection = false 314 | 315 | # Test override 316 | input = ["foo", "bar"] 317 | input.is_array_collection = true 318 | output = RocketAMF.serialize(input, 3) 319 | output.should == expected 320 | end 321 | 322 | it "should serialize a complex set of array collections" do 323 | RocketAMF::ClassMapper.define {|m| m.map :as => 'org.amf.ASClass', :ruby => 'RubyClass'} 324 | expected = object_fixture('amf3-complex-array-collection.bin') 325 | 326 | a = ["foo", "bar"] 327 | a.is_array_collection = true 328 | obj1 = RubyClass.new 329 | obj1.foo = "bar" 330 | def obj1.encode_amf serializer 331 | serializer.write_object(self, nil, {:class_name => 'org.amf.ASClass', :dynamic => false, :externalizable => false, :members => ["baz", "foo"]}) 332 | end 333 | obj2 = RubyClass.new 334 | obj2.foo = "asdf" 335 | def obj2.encode_amf serializer 336 | serializer.write_object(self, nil, {:class_name => 'org.amf.ASClass', :dynamic => false, :externalizable => false, :members => ["baz", "foo"]}) 337 | end 338 | b = [obj1, obj2] 339 | b.is_array_collection = true 340 | input = [a, b, b] 341 | 342 | output = RocketAMF.serialize(input, 3) 343 | output.should == expected 344 | end 345 | 346 | it "should serialize a byte array" do 347 | expected = object_fixture("amf3-byte-array.bin") 348 | str = "\000\003これtest\100" 349 | str.force_encoding("ASCII-8BIT") if str.respond_to?(:force_encoding) 350 | input = StringIO.new(str) 351 | output = RocketAMF.serialize(input, 3) 352 | output.should == expected 353 | end 354 | end 355 | 356 | describe "and implementing the AMF Spec" do 357 | it "should keep references of duplicate strings" do 358 | class StringCarrier 359 | attr_accessor :str 360 | end 361 | foo = "foo" 362 | bar = "str" 363 | sc = StringCarrier.new 364 | sc.str = foo 365 | 366 | expected = object_fixture("amf3-string-ref.bin") 367 | input = [foo, bar, foo, bar, foo, sc] 368 | output = RocketAMF.serialize(input, 3) 369 | output.should == expected 370 | end 371 | 372 | it "should not reference the empty string" do 373 | expected = object_fixture("amf3-empty-string-ref.bin") 374 | input = "" 375 | output = RocketAMF.serialize([input,input], 3) 376 | output.should == expected 377 | end 378 | 379 | it "should keep references of duplicate dates" do 380 | expected = object_fixture("amf3-date-ref.bin") 381 | input = Time.utc 1970, 1, 1, 0 382 | output = RocketAMF.serialize([input,input], 3) 383 | output.should == expected 384 | end 385 | 386 | it "should keep reference of duplicate objects" do 387 | class SimpleReferenceableObj 388 | attr_accessor :foo 389 | end 390 | obj1 = SimpleReferenceableObj.new 391 | obj1.foo = :bar 392 | obj2 = SimpleReferenceableObj.new 393 | obj2.foo = obj1.foo 394 | 395 | expected = object_fixture("amf3-object-ref.bin") 396 | input = [[obj1, obj2], "bar", [obj1, obj2]] 397 | output = RocketAMF.serialize(input, 3) 398 | output.should == expected 399 | end 400 | 401 | it "should keep reference of duplicate object traits" do 402 | obj1 = RubyClass.new 403 | obj1.foo = "foo" 404 | def obj1.encode_amf serializer 405 | serializer.write_object(self, nil, {:class_name => 'org.amf.ASClass', :dynamic => false, :externalizable => false, :members => ["baz", "foo"]}) 406 | end 407 | obj2 = RubyClass.new 408 | obj2.foo = "bar" 409 | def obj2.encode_amf serializer 410 | serializer.write_object(self, nil, {:class_name => 'org.amf.ASClass', :dynamic => false, :externalizable => false, :members => ["baz", "foo"]}) 411 | end 412 | input = [obj1, obj2] 413 | 414 | expected = object_fixture("amf3-trait-ref.bin") 415 | output = RocketAMF.serialize(input, 3) 416 | output.should == expected 417 | end 418 | 419 | it "should keep references of duplicate arrays" do 420 | a = [1,2,3] 421 | b = %w{ a b c } 422 | 423 | expected = object_fixture("amf3-array-ref.bin") 424 | input = [a, b, a, b] 425 | output = RocketAMF.serialize(input, 3) 426 | output.should == expected 427 | end 428 | 429 | it "should not keep references of duplicate empty arrays unless the object_id matches" do 430 | a = [] 431 | b = [] 432 | a.should == b 433 | a.object_id.should_not == b.object_id 434 | 435 | expected = object_fixture("amf3-empty-array-ref.bin") 436 | input = [a,b,a,b] 437 | output = RocketAMF.serialize(input, 3) 438 | output.should == expected 439 | end 440 | 441 | it "should keep references of duplicate byte arrays" do 442 | b = StringIO.new "ASDF" 443 | 444 | expected = object_fixture("amf3-byte-array-ref.bin") 445 | input = [b, b] 446 | output = RocketAMF.serialize(input, 3) 447 | output.should == expected 448 | end 449 | 450 | it "should serialize a deep object graph with circular references" do 451 | class GraphMember 452 | attr_accessor :children 453 | attr_accessor :parent 454 | 455 | def initialize 456 | self.children = [] 457 | end 458 | 459 | def add_child child 460 | children << child 461 | child.parent = self 462 | child 463 | end 464 | end 465 | 466 | parent = GraphMember.new 467 | level_1_child_1 = parent.add_child GraphMember.new 468 | level_1_child_2 = parent.add_child GraphMember.new 469 | 470 | expected = object_fixture("amf3-graph-member.bin") 471 | input = parent 472 | output = RocketAMF.serialize(input, 3) 473 | output.should == expected 474 | end 475 | end 476 | 477 | describe "and handling encodings", :if => "".respond_to?(:force_encoding) do 478 | it "should support multiple encodings" do 479 | shift_str = "\x53\x68\x69\x66\x74\x20\x83\x65\x83\x58\x83\x67".force_encoding("Shift_JIS") # "Shift テスト" 480 | utf_str = "\x55\x54\x46\x20\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88".force_encoding("UTF-8") # "UTF テスト" 481 | output = RocketAMF.serialize([5, shift_str, utf_str, 5], 3) 482 | output.should == object_fixture("amf3-complex-encoded-string-array.bin") 483 | end 484 | 485 | it "should keep references of duplicate strings with different encodings" do 486 | # String is "this is a テスト" 487 | shift_str = "\x74\x68\x69\x73\x20\x69\x73\x20\x61\x20\x83\x65\x83\x58\x83\x67".force_encoding("Shift_JIS") 488 | utf_str = "\x74\x68\x69\x73\x20\x69\x73\x20\x61\x20\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88".force_encoding("UTF-8") 489 | 490 | expected = object_fixture("amf3-encoded-string-ref.bin") 491 | output = RocketAMF.serialize([shift_str, utf_str], 3) 492 | output.should == expected 493 | end 494 | 495 | it "should handle inappropriate UTF-8 characters in byte arrays" do 496 | str = "\xff\xff\xff".force_encoding("ASCII-8BIT") 497 | str.freeze # For added amusement 498 | output = RocketAMF.serialize(StringIO.new(str), 3) 499 | output.should == "\x0c\x07\xff\xff\xff".force_encoding("ASCII-8BIT") 500 | end 501 | end 502 | end 503 | end -------------------------------------------------------------------------------- /ext/rocketamf_ext/deserializer.c: -------------------------------------------------------------------------------- 1 | #include "deserializer.h" 2 | #include "constants.h" 3 | 4 | #define DES_BOUNDS_CHECK(des, i) if(des->pos + (i) > des->size || des->pos + (i) < des->pos) rb_raise(rb_eRangeError, "reading %lu bytes is beyond end of source: %ld (pos), %ld (size)", (unsigned long)(i), des->pos, des->size); 5 | 6 | extern VALUE mRocketAMF; 7 | extern VALUE mRocketAMFExt; 8 | extern VALUE cDeserializer; 9 | extern VALUE cStringIO; 10 | extern VALUE sym_class_name; 11 | extern VALUE sym_members; 12 | extern VALUE sym_externalizable; 13 | extern VALUE sym_dynamic; 14 | ID id_get_ruby_obj; 15 | ID id_populate_ruby_obj; 16 | 17 | static VALUE des0_deserialize(VALUE self, char type); 18 | static VALUE des3_deserialize(VALUE self); 19 | 20 | char des_read_byte(AMF_DESERIALIZER *des) { 21 | DES_BOUNDS_CHECK(des, 1); 22 | des->pos++; 23 | return des->stream[des->pos-1]; 24 | } 25 | 26 | char des_read_ahead_byte(AMF_DESERIALIZER *des) { 27 | DES_BOUNDS_CHECK(des, 1); 28 | return des->stream[des->pos]; 29 | } 30 | 31 | int des_read_uint16(AMF_DESERIALIZER *des) { 32 | DES_BOUNDS_CHECK(des, 2); 33 | const unsigned char *str = (unsigned char*)(des->stream) + des->pos; 34 | des->pos += 2; 35 | return ((str[0] << 8) | str[1]); 36 | } 37 | 38 | unsigned int des_read_uint32(AMF_DESERIALIZER *des) { 39 | DES_BOUNDS_CHECK(des, 4); 40 | const unsigned char *str = (unsigned char*)(des->stream) + des->pos; 41 | des->pos += 4; 42 | return ((str[0] << 24) | (str[1] << 16) | (str[2] << 8) | str[3]); 43 | } 44 | 45 | /* 46 | * Read a network double 47 | */ 48 | double des_read_double(AMF_DESERIALIZER *des) { 49 | DES_BOUNDS_CHECK(des, 8); 50 | union aligned { 51 | double dval; 52 | char cval[8]; 53 | } d; 54 | const char *str = des->stream + des->pos; 55 | des->pos +=8; 56 | 57 | #ifdef WORDS_BIGENDIAN 58 | memcpy(d.cval, str, 8); 59 | #else 60 | d.cval[0] = str[7]; 61 | d.cval[1] = str[6]; 62 | d.cval[2] = str[5]; 63 | d.cval[3] = str[4]; 64 | d.cval[4] = str[3]; 65 | d.cval[5] = str[2]; 66 | d.cval[6] = str[1]; 67 | d.cval[7] = str[0]; 68 | #endif 69 | return d.dval; 70 | } 71 | 72 | /* 73 | * Read an AMF3 style integer 74 | */ 75 | int des_read_int(AMF_DESERIALIZER *des) { 76 | int result = 0, byte_cnt = 0; 77 | DES_BOUNDS_CHECK(des, 1); 78 | unsigned char byte = des->stream[des->pos++]; 79 | 80 | while(byte & 0x80 && byte_cnt < 3) { 81 | result <<= 7; 82 | result |= byte & 0x7f; 83 | DES_BOUNDS_CHECK(des, 1); 84 | byte = des->stream[des->pos++]; 85 | byte_cnt++; 86 | } 87 | 88 | if (byte_cnt < 3) { 89 | result <<= 7; 90 | result |= byte & 0x7F; 91 | } else { 92 | result <<= 8; 93 | result |= byte & 0xff; 94 | } 95 | 96 | if (result & 0x10000000) { 97 | result -= 0x20000000; 98 | } 99 | 100 | return result; 101 | } 102 | 103 | /* 104 | * Read a string and then force the encoding to UTF 8 if running ruby 1.9 105 | */ 106 | VALUE des_read_string(AMF_DESERIALIZER *des, unsigned int len) { 107 | DES_BOUNDS_CHECK(des, len); 108 | VALUE str = rb_str_new(des->stream + des->pos, len); 109 | #ifdef HAVE_RB_STR_ENCODE 110 | rb_encoding *utf8 = rb_utf8_encoding(); 111 | rb_enc_associate(str, utf8); 112 | ENC_CODERANGE_CLEAR(str); 113 | #endif 114 | des->pos += len; 115 | return str; 116 | } 117 | 118 | /* 119 | * Set the source of the amf reader to a StringIO object, creating a new one to 120 | * wrap the source if it's only a string 121 | */ 122 | void des_set_src(AMF_DESERIALIZER *des, VALUE src) { 123 | VALUE klass = CLASS_OF(src); 124 | if(klass == cStringIO) { 125 | VALUE str = rb_funcall(src, rb_intern("string"), 0); 126 | des->src = src; 127 | des->stream = RSTRING_PTR(str); 128 | des->pos = NUM2LONG(rb_funcall(src, rb_intern("pos"), 0)); 129 | des->size = RSTRING_LEN(str); 130 | } else if(klass == rb_cString) { 131 | VALUE args[1] = {src}; 132 | des->src = rb_class_new_instance(1, args, cStringIO); 133 | des->stream = RSTRING_PTR(src); 134 | des->pos = 0; 135 | des->size = RSTRING_LEN(src); 136 | } else { 137 | rb_raise(rb_eArgError, "Invalid source type to deserialize from"); 138 | } 139 | 140 | if(des->pos >= des->size) rb_raise(rb_eRangeError, "already at the end of the source"); 141 | } 142 | 143 | /* 144 | * Create AMF3 deserializer and copy source data over to it, before calling 145 | * AMF3 internal deserialize function 146 | */ 147 | static VALUE des0_read_amf3(VALUE self) { 148 | AMF_DESERIALIZER *des; 149 | Data_Get_Struct(self, AMF_DESERIALIZER, des); 150 | des->version = 3; 151 | des->str_cache = rb_ary_new(); 152 | des->trait_cache = rb_ary_new(); 153 | return des3_deserialize(self); 154 | } 155 | 156 | /* 157 | * Reads an AMF0 hash 158 | */ 159 | static void des0_read_props(VALUE self, VALUE hash) { 160 | AMF_DESERIALIZER *des; 161 | Data_Get_Struct(self, AMF_DESERIALIZER, des); 162 | 163 | while(1) { 164 | int len = des_read_uint16(des); 165 | if(len == 0 && des_read_ahead_byte(des) == AMF0_OBJECT_END_MARKER) { 166 | // Don't create a ruby string if this is really the object end 167 | des_read_byte(des); // Read type byte 168 | return; 169 | } else { 170 | VALUE key = des_read_string(des, len); 171 | char type = des_read_byte(des); 172 | rb_hash_aset(hash, key, des0_deserialize(self, type)); 173 | } 174 | } 175 | } 176 | 177 | static VALUE des0_read_object(VALUE self) { 178 | AMF_DESERIALIZER *des; 179 | Data_Get_Struct(self, AMF_DESERIALIZER, des); 180 | 181 | // Create object and add to cache 182 | VALUE obj = rb_funcall(des->class_mapper, id_get_ruby_obj, 1, rb_str_new(NULL, 0)); 183 | rb_ary_push(des->obj_cache, obj); 184 | 185 | // Populate object 186 | VALUE props = rb_hash_new(); 187 | des0_read_props(self, props); 188 | rb_funcall(des->class_mapper, id_populate_ruby_obj, 2, obj, props); 189 | 190 | return obj; 191 | } 192 | 193 | static VALUE des0_read_typed_object(VALUE self) { 194 | AMF_DESERIALIZER *des; 195 | Data_Get_Struct(self, AMF_DESERIALIZER, des); 196 | 197 | // Create object and add to cache 198 | VALUE class_name = des_read_string(des, des_read_uint16(des)); 199 | VALUE obj = rb_funcall(des->class_mapper, id_get_ruby_obj, 1, class_name); 200 | rb_ary_push(des->obj_cache, obj); 201 | 202 | // Populate object 203 | VALUE props = rb_hash_new(); 204 | des0_read_props(self, props); 205 | rb_funcall(des->class_mapper, id_populate_ruby_obj, 2, obj, props); 206 | 207 | return obj; 208 | } 209 | 210 | static VALUE des0_read_hash(VALUE self) { 211 | AMF_DESERIALIZER *des; 212 | Data_Get_Struct(self, AMF_DESERIALIZER, des); 213 | des_read_uint32(des); // Hash size, but there's no optimization I can perform with this 214 | VALUE obj = rb_hash_new(); 215 | rb_ary_push(des->obj_cache, obj); 216 | des0_read_props(self, obj); 217 | return obj; 218 | } 219 | 220 | static VALUE des0_read_array(VALUE self) { 221 | AMF_DESERIALIZER *des; 222 | Data_Get_Struct(self, AMF_DESERIALIZER, des); 223 | 224 | // Limit size of pre-allocation to force remote user to actually send data, 225 | // rather than just sending a size of 2**32-1 and nothing afterwards to 226 | // crash the server 227 | unsigned int len = des_read_uint32(des); 228 | VALUE ary = rb_ary_new2(len < MAX_ARRAY_PREALLOC ? len : MAX_ARRAY_PREALLOC); 229 | rb_ary_push(des->obj_cache, ary); 230 | 231 | unsigned int i; 232 | for(i = 0; i < len; i++) { 233 | rb_ary_push(ary, des0_deserialize(self, des_read_byte(des))); 234 | } 235 | 236 | return ary; 237 | } 238 | 239 | static VALUE des0_read_time(VALUE self) { 240 | AMF_DESERIALIZER *des; 241 | Data_Get_Struct(self, AMF_DESERIALIZER, des); 242 | double milli = des_read_double(des); 243 | des_read_uint16(des); // Timezone - unused 244 | time_t sec = milli/1000.0; 245 | time_t micro = (milli-sec*1000)*1000; 246 | return rb_time_new(sec, micro); 247 | } 248 | 249 | /* 250 | * Internal C deserialize call. Takes deserializer and a char for the type 251 | * marker. 252 | */ 253 | static VALUE des0_deserialize(VALUE self, char type) { 254 | AMF_DESERIALIZER *des; 255 | Data_Get_Struct(self, AMF_DESERIALIZER, des); 256 | 257 | long tmp; 258 | VALUE ret = Qnil; 259 | switch(type) { 260 | case AMF0_STRING_MARKER: 261 | ret = des_read_string(des, des_read_uint16(des)); 262 | break; 263 | case AMF0_AMF3_MARKER: 264 | ret = des0_read_amf3(self); 265 | break; 266 | case AMF0_NUMBER_MARKER: 267 | ret = rb_float_new(des_read_double(des)); 268 | break; 269 | case AMF0_BOOLEAN_MARKER: 270 | ret = des_read_byte(des) == 0 ? Qfalse : Qtrue; 271 | break; 272 | case AMF0_NULL_MARKER: 273 | case AMF0_UNDEFINED_MARKER: 274 | case AMF0_UNSUPPORTED_MARKER: 275 | ret = Qnil; 276 | break; 277 | case AMF0_OBJECT_MARKER: 278 | ret = des0_read_object(self); 279 | break; 280 | case AMF0_TYPED_OBJECT_MARKER: 281 | ret = des0_read_typed_object(self); 282 | break; 283 | case AMF0_HASH_MARKER: 284 | ret = des0_read_hash(self); 285 | break; 286 | case AMF0_STRICT_ARRAY_MARKER: 287 | ret = des0_read_array(self); 288 | break; 289 | case AMF0_REFERENCE_MARKER: 290 | tmp = des_read_uint16(des); 291 | if(tmp >= RARRAY_LEN(des->obj_cache)) rb_raise(rb_eRangeError, "reference index beyond end"); 292 | ret = RARRAY_PTR(des->obj_cache)[tmp]; 293 | break; 294 | case AMF0_DATE_MARKER: 295 | ret = des0_read_time(self); 296 | break; 297 | case AMF0_XML_MARKER: 298 | case AMF0_LONG_STRING_MARKER: 299 | ret = des_read_string(des, des_read_uint32(des)); 300 | break; 301 | default: 302 | rb_raise(rb_eRuntimeError, "Not supported: %d", type); 303 | break; 304 | } 305 | 306 | return ret; 307 | } 308 | 309 | static VALUE des3_read_string(AMF_DESERIALIZER *des) { 310 | int header = des_read_int(des); 311 | if((header & 1) == 0) { 312 | header >>= 1; 313 | if(header >= RARRAY_LEN(des->str_cache)) rb_raise(rb_eRangeError, "str reference index beyond end"); 314 | return RARRAY_PTR(des->str_cache)[header]; 315 | } else { 316 | VALUE str = des_read_string(des, header >> 1); 317 | if(RSTRING_LEN(str) > 0) rb_ary_push(des->str_cache, str); 318 | return str; 319 | } 320 | } 321 | 322 | /* 323 | * Same as des3_read_string, but XML uses the object cache, rather than the 324 | * string cache 325 | */ 326 | static VALUE des3_read_xml(VALUE self) { 327 | AMF_DESERIALIZER *des; 328 | Data_Get_Struct(self, AMF_DESERIALIZER, des); 329 | 330 | int header = des_read_int(des); 331 | if((header & 1) == 0) { 332 | header >>= 1; 333 | if(header >= RARRAY_LEN(des->obj_cache)) rb_raise(rb_eRangeError, "obj reference index beyond end"); 334 | return RARRAY_PTR(des->obj_cache)[header]; 335 | } else { 336 | VALUE str = des_read_string(des, header >> 1); 337 | if(RSTRING_LEN(str) > 0) rb_ary_push(des->obj_cache, str); 338 | return str; 339 | } 340 | } 341 | 342 | static VALUE des3_read_object(VALUE self) { 343 | AMF_DESERIALIZER *des; 344 | Data_Get_Struct(self, AMF_DESERIALIZER, des); 345 | 346 | int header = des_read_int(des); 347 | if((header & 1) == 0) { 348 | header >>= 1; 349 | if(header >= RARRAY_LEN(des->obj_cache)) rb_raise(rb_eRangeError, "obj reference index beyond end"); 350 | return RARRAY_PTR(des->obj_cache)[header]; 351 | } else { 352 | VALUE externalizable, dynamic, members, class_name, traits; 353 | long i, members_len; 354 | 355 | // Parse traits 356 | header >>= 1; 357 | if((header & 1) == 0) { 358 | header >>= 1; 359 | if(header >= RARRAY_LEN(des->trait_cache)) rb_raise(rb_eRangeError, "trait reference index beyond end"); 360 | traits = RARRAY_PTR(des->trait_cache)[header]; 361 | externalizable = rb_hash_aref(traits, sym_externalizable); 362 | dynamic = rb_hash_aref(traits, sym_dynamic); 363 | members = rb_hash_aref(traits, sym_members); 364 | members_len = members == Qnil ? 0 : RARRAY_LEN(members); 365 | class_name = rb_hash_aref(traits, sym_class_name); 366 | } else { 367 | externalizable = (header & 2) != 0 ? Qtrue : Qfalse; 368 | dynamic = (header & 4) != 0 ? Qtrue : Qfalse; 369 | members_len = header >> 3; 370 | class_name = des3_read_string(des); 371 | 372 | members = rb_ary_new2(members_len); 373 | for(i = 0; i < members_len; i++) rb_ary_push(members, des3_read_string(des)); 374 | 375 | traits = rb_hash_new(); 376 | rb_hash_aset(traits, sym_externalizable, externalizable); 377 | rb_hash_aset(traits, sym_dynamic, dynamic); 378 | rb_hash_aset(traits, sym_members, members); 379 | rb_hash_aset(traits, sym_class_name, class_name); 380 | rb_ary_push(des->trait_cache, traits); 381 | } 382 | 383 | // Optimization for deserializing ArrayCollection 384 | if(strcmp(RSTRING_PTR(class_name), "flex.messaging.io.ArrayCollection") == 0) { 385 | VALUE arr = des3_deserialize(self); // Adds ArrayCollection array to object cache automatically 386 | rb_ary_push(des->obj_cache, arr); // Add again for ArrayCollection source array 387 | return arr; 388 | } 389 | 390 | VALUE obj = rb_funcall(des->class_mapper, id_get_ruby_obj, 1, class_name); 391 | rb_ary_push(des->obj_cache, obj); 392 | 393 | if(externalizable == Qtrue) { 394 | rb_funcall(des->src, rb_intern("pos="), 1, LONG2NUM(des->pos)); // Update source StringIO pos 395 | rb_funcall(obj, rb_intern("read_external"), 1, self); 396 | des->pos = NUM2LONG(rb_funcall(des->src, rb_intern("pos"), 0)); // Update from source 397 | return obj; 398 | } 399 | 400 | VALUE props = rb_hash_new(); 401 | for(i = 0; i < members_len; i++) { 402 | rb_hash_aset(props, RARRAY_PTR(members)[i], des3_deserialize(self)); 403 | } 404 | 405 | VALUE dynamic_props = Qnil; 406 | if(dynamic == Qtrue) { 407 | dynamic_props = rb_hash_new(); 408 | while(1) { 409 | VALUE key = des3_read_string(des); 410 | if(RSTRING_LEN(key) == 0) break; 411 | rb_hash_aset(dynamic_props, key, des3_deserialize(self)); 412 | } 413 | } 414 | 415 | rb_funcall(des->class_mapper, id_populate_ruby_obj, 3, obj, props, dynamic_props); 416 | 417 | return obj; 418 | } 419 | } 420 | 421 | static VALUE des3_read_array(VALUE self) { 422 | AMF_DESERIALIZER *des; 423 | Data_Get_Struct(self, AMF_DESERIALIZER, des); 424 | 425 | int i; 426 | int header = des_read_int(des); 427 | if((header & 1) == 0) { 428 | header >>= 1; 429 | if(header >= RARRAY_LEN(des->obj_cache)) rb_raise(rb_eRangeError, "obj reference index beyond end"); 430 | return RARRAY_PTR(des->obj_cache)[header]; 431 | } else { 432 | header >>= 1; 433 | VALUE obj; 434 | VALUE key = des3_read_string(des); 435 | if(key == Qnil) rb_raise(rb_eRangeError, "key is Qnil"); 436 | if(RSTRING_LEN(key) != 0) { 437 | obj = rb_hash_new(); 438 | rb_ary_push(des->obj_cache, obj); 439 | while(RSTRING_LEN(key) != 0) { 440 | rb_hash_aset(obj, key, des3_deserialize(self)); 441 | key = des3_read_string(des); 442 | } 443 | for(i = 0; i < header; i++) { 444 | rb_hash_aset(obj, INT2FIX(i), des3_deserialize(self)); 445 | } 446 | } else { 447 | // Limit size of pre-allocation to force remote user to actually send data, 448 | // rather than just sending a size of 2**32-1 and nothing afterwards to 449 | // crash the server 450 | obj = rb_ary_new2(header < MAX_ARRAY_PREALLOC ? header : MAX_ARRAY_PREALLOC); 451 | rb_ary_push(des->obj_cache, obj); 452 | for(i = 0; i < header; i++) { 453 | rb_ary_push(obj, des3_deserialize(self)); 454 | } 455 | } 456 | return obj; 457 | } 458 | } 459 | 460 | static VALUE des3_read_time(VALUE self) { 461 | AMF_DESERIALIZER *des; 462 | Data_Get_Struct(self, AMF_DESERIALIZER, des); 463 | 464 | int header = des_read_int(des); 465 | if((header & 1) == 0) { 466 | header >>= 1; 467 | if(header >= RARRAY_LEN(des->obj_cache)) rb_raise(rb_eRangeError, "obj reference index beyond end"); 468 | return RARRAY_PTR(des->obj_cache)[header]; 469 | } else { 470 | double milli = des_read_double(des); 471 | time_t sec = milli/1000.0; 472 | time_t micro = (milli-sec*1000)*1000; 473 | VALUE time = rb_time_new(sec, micro); 474 | rb_ary_push(des->obj_cache, time); 475 | return time; 476 | } 477 | } 478 | 479 | static VALUE des3_read_byte_array(VALUE self) { 480 | AMF_DESERIALIZER *des; 481 | Data_Get_Struct(self, AMF_DESERIALIZER, des); 482 | 483 | int header = des_read_int(des); 484 | if((header & 1) == 0) { 485 | header >>= 1; 486 | if(header >= RARRAY_LEN(des->obj_cache)) rb_raise(rb_eRangeError, "obj reference index beyond end"); 487 | return RARRAY_PTR(des->obj_cache)[header]; 488 | } else { 489 | header >>= 1; 490 | VALUE args[1] = {des_read_string(des, header)}; 491 | #ifdef HAVE_RB_STR_ENCODE 492 | // Need to force encoding to ASCII-8BIT 493 | rb_encoding *ascii = rb_ascii8bit_encoding(); 494 | rb_enc_associate(args[0], ascii); 495 | ENC_CODERANGE_CLEAR(args[0]); 496 | #endif 497 | VALUE ba = rb_class_new_instance(1, args, cStringIO); 498 | rb_ary_push(des->obj_cache, ba); 499 | return ba; 500 | } 501 | } 502 | 503 | static VALUE des3_read_dict(VALUE self) { 504 | AMF_DESERIALIZER *des; 505 | Data_Get_Struct(self, AMF_DESERIALIZER, des); 506 | 507 | int header = des_read_int(des); 508 | if((header & 1) == 0) { 509 | header >>= 1; 510 | if(header >= RARRAY_LEN(des->obj_cache)) rb_raise(rb_eRangeError, "obj reference index beyond end"); 511 | return RARRAY_PTR(des->obj_cache)[header]; 512 | } else { 513 | header >>= 1; 514 | 515 | VALUE dict = rb_hash_new(); 516 | rb_ary_push(des->obj_cache, dict); 517 | 518 | des_read_byte(des); // Weak Keys: Not supported in ruby 519 | 520 | int i; 521 | for(i = 0; i < header; i++) { 522 | VALUE key = des3_deserialize(self); 523 | VALUE val = des3_deserialize(self); 524 | rb_hash_aset(dict, key, val); 525 | } 526 | 527 | return dict; 528 | } 529 | } 530 | 531 | static VALUE des3_read_vec(VALUE self, char type) { 532 | AMF_DESERIALIZER *des; 533 | Data_Get_Struct(self, AMF_DESERIALIZER, des); 534 | 535 | int header = des_read_int(des); 536 | if((header & 1) == 0) { 537 | header >>= 1; 538 | if(header >= RARRAY_LEN(des->obj_cache)) rb_raise(rb_eRangeError, "obj reference index beyond end"); 539 | return RARRAY_PTR(des->obj_cache)[header]; 540 | } else { 541 | header >>= 1; 542 | 543 | // Limit size of pre-allocation to force remote user to actually send data, 544 | // rather than just sending a size of 2**32-1 and nothing afterwards to 545 | // crash the server 546 | VALUE vec = rb_ary_new2(header < MAX_ARRAY_PREALLOC ? header : MAX_ARRAY_PREALLOC); 547 | rb_ary_push(des->obj_cache, vec); 548 | 549 | des_read_byte(des); // Fixed Length: Not supported in ruby 550 | 551 | // On 32-bit ARCH, FIXNUM has a limit of 2**31-1, resulting in truncation of large ints/uints 552 | int i; 553 | switch(type) { 554 | case AMF3_VECTOR_INT_MARKER: 555 | for(i = 0; i < header; i++) { 556 | int ival = des_read_uint32(des); 557 | rb_ary_push(vec, INT2FIX(ival)); 558 | } 559 | break; 560 | case AMF3_VECTOR_UINT_MARKER: 561 | for(i = 0; i < header; i++) { 562 | rb_ary_push(vec, INT2FIX(des_read_uint32(des))); 563 | } 564 | break; 565 | case AMF3_VECTOR_DOUBLE_MARKER: 566 | for(i = 0; i < header; i++) { 567 | rb_ary_push(vec, rb_float_new(des_read_double(des))); 568 | } 569 | break; 570 | case AMF3_VECTOR_OBJECT_MARKER: 571 | des3_read_string(des); // Class name of objects - ignored 572 | for(i = 0; i < header; i++) { 573 | rb_ary_push(vec, des3_deserialize(self)); 574 | } 575 | break; 576 | } 577 | return vec; 578 | } 579 | } 580 | 581 | /* 582 | * Internal deserialize call - unlike des0_deserialize, it reads the type 583 | * itself, due to minor changes in the specs that make that modification 584 | * unnecessary. 585 | */ 586 | static VALUE des3_deserialize(VALUE self) { 587 | AMF_DESERIALIZER *des; 588 | Data_Get_Struct(self, AMF_DESERIALIZER, des); 589 | 590 | char type = des_read_byte(des); 591 | VALUE ret = Qnil; 592 | switch(type) { 593 | case AMF3_UNDEFINED_MARKER: 594 | case AMF3_NULL_MARKER: 595 | ret = Qnil; 596 | break; 597 | case AMF3_FALSE_MARKER: 598 | ret = Qfalse; 599 | break; 600 | case AMF3_TRUE_MARKER: 601 | ret = Qtrue; 602 | break; 603 | case AMF3_INTEGER_MARKER: 604 | ret = INT2FIX(des_read_int(des)); 605 | break; 606 | case AMF3_DOUBLE_MARKER: 607 | ret = rb_float_new(des_read_double(des)); 608 | break; 609 | case AMF3_STRING_MARKER: 610 | ret = des3_read_string(des); 611 | break; 612 | case AMF3_ARRAY_MARKER: 613 | ret = des3_read_array(self); 614 | break; 615 | case AMF3_OBJECT_MARKER: 616 | ret = des3_read_object(self); 617 | break; 618 | case AMF3_DATE_MARKER: 619 | ret = des3_read_time(self); 620 | break; 621 | case AMF3_XML_DOC_MARKER: 622 | case AMF3_XML_MARKER: 623 | ret = des3_read_xml(self); 624 | break; 625 | case AMF3_BYTE_ARRAY_MARKER: 626 | ret = des3_read_byte_array(self); 627 | break; 628 | case AMF3_VECTOR_INT_MARKER: 629 | case AMF3_VECTOR_UINT_MARKER: 630 | case AMF3_VECTOR_DOUBLE_MARKER: 631 | case AMF3_VECTOR_OBJECT_MARKER: 632 | ret = des3_read_vec(self, type); 633 | break; 634 | case AMF3_DICT_MARKER: 635 | ret = des3_read_dict(self); 636 | break; 637 | default: 638 | rb_raise(rb_eRuntimeError, "Not supported: %d", type); 639 | break; 640 | } 641 | 642 | return ret; 643 | } 644 | 645 | /* 646 | * Mark the reader and its source. If caches are populated mark them as well. 647 | */ 648 | static void des_mark(AMF_DESERIALIZER *des) { 649 | if(!des) return; 650 | rb_gc_mark(des->class_mapper); 651 | rb_gc_mark(des->src); 652 | if(des->obj_cache) rb_gc_mark(des->obj_cache); 653 | if(des->str_cache) rb_gc_mark(des->str_cache); 654 | if(des->trait_cache) rb_gc_mark(des->trait_cache); 655 | } 656 | 657 | /* 658 | * Free the reader. Don't need to free anything but the struct because we didn't 659 | * alloc anything - source is from the ruby source object. 660 | */ 661 | static void des_free(AMF_DESERIALIZER *des) { 662 | xfree(des); 663 | } 664 | 665 | /* 666 | * Create new struct and wrap with class 667 | */ 668 | static VALUE des_alloc(VALUE klass) { 669 | AMF_DESERIALIZER *des = ALLOC(AMF_DESERIALIZER); 670 | memset(des, 0, sizeof(AMF_DESERIALIZER)); 671 | return Data_Wrap_Struct(klass, des_mark, des_free, des); 672 | } 673 | 674 | /* 675 | * Initializer 676 | */ 677 | static VALUE des_initialize(VALUE self, VALUE class_mapper) { 678 | AMF_DESERIALIZER *des; 679 | Data_Get_Struct(self, AMF_DESERIALIZER, des); 680 | des->class_mapper = class_mapper; 681 | return self; 682 | } 683 | 684 | /* 685 | * call-seq: 686 | * ser.stream => StringIO 687 | * 688 | * Returns the source that the deserializer is reading from 689 | */ 690 | static VALUE des_source(VALUE self) { 691 | AMF_DESERIALIZER *des; 692 | Data_Get_Struct(self, AMF_DESERIALIZER, des); 693 | return des->src; 694 | } 695 | 696 | /* 697 | * call-seq: 698 | * des.deserialize(amf_ver, str) => obj 699 | * des.deserialize(amf_ver, StringIO) => obj 700 | * 701 | * Deserialize the string or StringIO from AMF to a ruby object. 702 | */ 703 | VALUE des_deserialize(VALUE self, VALUE ver, VALUE src) { 704 | AMF_DESERIALIZER *des; 705 | Data_Get_Struct(self, AMF_DESERIALIZER, des); 706 | 707 | // Process version 708 | int int_ver = FIX2INT(ver); 709 | if(int_ver != 0 && int_ver != 3) rb_raise(rb_eArgError, "unsupported version %d", int_ver); 710 | des->version = int_ver; 711 | 712 | // Process source 713 | if(src != Qnil) { 714 | des_set_src(des, src); 715 | } else if(!des->src) { 716 | rb_raise(rb_eArgError, "Missing deserialization source"); 717 | } 718 | 719 | // Deserialize from source 720 | VALUE ret; 721 | if(des->version == 0) { 722 | des->obj_cache = rb_ary_new(); 723 | ret = des0_deserialize(self, des_read_byte(des)); 724 | } else { 725 | des->obj_cache = rb_ary_new(); 726 | des->str_cache = rb_ary_new(); 727 | des->trait_cache = rb_ary_new(); 728 | ret = des3_deserialize(self); 729 | } 730 | 731 | // Update source position 732 | rb_funcall(des->src, rb_intern("pos="), 1, LONG2NUM(des->pos)); // Update source StringIO pos 733 | 734 | return ret; 735 | } 736 | 737 | /* 738 | * call-seq: 739 | * des.read_object => obj 740 | * 741 | * Reads an object from the deserializer's stream and returns it. 742 | */ 743 | VALUE des_read_object(VALUE self) { 744 | AMF_DESERIALIZER *des; 745 | Data_Get_Struct(self, AMF_DESERIALIZER, des); 746 | 747 | // Update internal pos from source in case they've modified it 748 | des->pos = NUM2LONG(rb_funcall(des->src, rb_intern("pos"), 0)); 749 | 750 | // Deserialize 751 | VALUE ret; 752 | if(des->version == 0) { 753 | ret = des0_deserialize(self, des_read_byte(des)); 754 | } else { 755 | ret = des3_deserialize(self); 756 | } 757 | 758 | // Update source position 759 | rb_funcall(des->src, rb_intern("pos="), 1, LONG2NUM(des->pos)); // Update source StringIO pos 760 | 761 | return ret; 762 | } 763 | 764 | void Init_rocket_amf_deserializer() { 765 | // Define Deserializer 766 | cDeserializer = rb_define_class_under(mRocketAMFExt, "Deserializer", rb_cObject); 767 | rb_define_alloc_func(cDeserializer, des_alloc); 768 | rb_define_method(cDeserializer, "initialize", des_initialize, 1); 769 | rb_define_method(cDeserializer, "source", des_source, 0); 770 | rb_define_method(cDeserializer, "deserialize", des_deserialize, 2); 771 | rb_define_method(cDeserializer, "read_object", des_read_object, 0); 772 | 773 | // Get refs to commonly used symbols and ids 774 | id_get_ruby_obj = rb_intern("get_ruby_obj"); 775 | id_populate_ruby_obj = rb_intern("populate_ruby_obj"); 776 | } --------------------------------------------------------------------------------