├── 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 | a b c d
--------------------------------------------------------------------------------
/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 | a b c d
--------------------------------------------------------------------------------
/spec/fixtures/objects/amf0-untyped-object.bin:
--------------------------------------------------------------------------------
1 | baz foo bar
--------------------------------------------------------------------------------
/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 | c d a b last
--------------------------------------------------------------------------------
/spec/fixtures/objects/amf0-typed-object.bin:
--------------------------------------------------------------------------------
1 | org.amf.ASClass baz foo bar
--------------------------------------------------------------------------------
/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 | 0 a 1 b 2 c 3 d
--------------------------------------------------------------------------------
/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 | barasdf1
2 | #org.amf.ASClassbazfooasdf2
--------------------------------------------------------------------------------
/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 | shift Shift テスト 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.