ivanvanderbyl-amqp 0.6.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +4 -0
- data/HISTORY +27 -0
- data/README.md +169 -0
- data/Rakefile +24 -0
- data/TODO +32 -0
- data/VERSION +1 -0
- data/doc/EXAMPLE_01_PINGPONG +2 -0
- data/doc/EXAMPLE_02_CLOCK +2 -0
- data/doc/EXAMPLE_03_STOCKS +2 -0
- data/doc/EXAMPLE_04_MULTICLOCK +2 -0
- data/doc/EXAMPLE_05_ACK +2 -0
- data/doc/EXAMPLE_05_POP +2 -0
- data/doc/EXAMPLE_06_HASHTABLE +2 -0
- data/lib/amqp.rb +110 -0
- data/lib/amqp/buffer.rb +270 -0
- data/lib/amqp/client.rb +225 -0
- data/lib/amqp/frame.rb +66 -0
- data/lib/amqp/protocol.rb +161 -0
- data/lib/amqp/server.rb +99 -0
- data/lib/amqp/spec.rb +832 -0
- data/lib/amqp/version.rb +6 -0
- data/lib/ext/blankslate.rb +7 -0
- data/lib/ext/em.rb +8 -0
- data/lib/ext/emfork.rb +69 -0
- data/lib/mq.rb +875 -0
- data/lib/mq/exchange.rb +351 -0
- data/lib/mq/header.rb +33 -0
- data/lib/mq/logger.rb +89 -0
- data/lib/mq/queue.rb +455 -0
- data/lib/mq/rpc.rb +100 -0
- data/old/README +30 -0
- data/old/Rakefile +12 -0
- data/old/amqp-0.8.json +606 -0
- data/old/amqp_spec.rb +796 -0
- data/old/amqpc.rb +695 -0
- data/old/codegen.rb +148 -0
- data/protocol/amqp-0.8.json +617 -0
- data/protocol/amqp-0.8.xml +3908 -0
- data/protocol/codegen.rb +173 -0
- data/protocol/doc.txt +281 -0
- data/research/api.rb +88 -0
- data/research/primes-forked.rb +63 -0
- data/research/primes-processes.rb +135 -0
- data/research/primes-threaded.rb +49 -0
- data/tasks/common.rake +18 -0
- data/tasks/doc.rake +14 -0
- data/tasks/gem.rake +40 -0
- data/tasks/git.rake +34 -0
- data/tasks/spec.rake +15 -0
- data/tasks/version.rake +71 -0
- metadata +158 -0
data/.gitignore
ADDED
data/HISTORY
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
== 0.6.7 / 2010-11-03
|
2
|
+
|
3
|
+
* Forked tmm1/amqp at Github
|
4
|
+
|
5
|
+
== 0.6.8 / 2010-11-03
|
6
|
+
|
7
|
+
* Additional improvements from latest forks added
|
8
|
+
|
9
|
+
== 0.6.9 / 2010-11-04
|
10
|
+
|
11
|
+
* Exchange#publish raises error if no connection to broker
|
12
|
+
|
13
|
+
== 0.6.10 / 2010-11-04
|
14
|
+
|
15
|
+
* Bacon tests converted to Rspec
|
16
|
+
|
17
|
+
== 0.6.11 / 2010-11-04
|
18
|
+
|
19
|
+
* AMQP class behavior specified
|
20
|
+
|
21
|
+
== 0.6.12 / 2010-11-04
|
22
|
+
|
23
|
+
* MQ class methods specified
|
24
|
+
|
25
|
+
== 0.6.13 / 2010-11-05
|
26
|
+
|
27
|
+
* Secondary MQ instance methods specified
|
data/README.md
ADDED
@@ -0,0 +1,169 @@
|
|
1
|
+
What AMQP gem is
|
2
|
+
================
|
3
|
+
|
4
|
+
Simple asynchronous AMQP driver for Ruby/EventMachine
|
5
|
+
This library works with Ruby 1.8, Ruby 1.9, JRuby and Rubinius,
|
6
|
+
and is licensed under [the Ruby License](http://www.ruby-lang.org/en/LICENSE.txt).
|
7
|
+
|
8
|
+
This library was tested primarily with RabbitMQ, although it should be compatible with any
|
9
|
+
server implementing the [AMQP 0-8 spec](http://www.amqp.org/confluence/download/attachments/720900/amqp0-8.pdf).
|
10
|
+
|
11
|
+
|
12
|
+
This fork of AMQP
|
13
|
+
=================
|
14
|
+
|
15
|
+
Intention is to describe AMQP lib behavior using RSpec, with reasonable level of completeness.
|
16
|
+
Given a solid spec suite, it will be easier to expand the library going forward.
|
17
|
+
|
18
|
+
Some improvements that are currently merged in, but not yet fully specified:
|
19
|
+
|
20
|
+
* Support for setting extended headers in MQ::Exchange#publish. Particularly useful for :reply_to for RPC.
|
21
|
+
* Multibyte character support (Ruby 1.9) in MQ::Exchange#publish.
|
22
|
+
* MQ::Exchange#publish raises error if no connection to broker.
|
23
|
+
* MQ::Queue only wraps headers with new MQ::Headers if they are not nil. This allows pops to tell more easily when they've requested a message from an empty queue. See (https://github.com/tmm1/amqp/issues#issue/22)
|
24
|
+
* Support for receiving Headers with zero-size data packets. Such contents with no body frames are totally legit if indicated header size is zero.
|
25
|
+
|
26
|
+
Some improvements that are planned:
|
27
|
+
* Support for AMQP::Protocol::Basic::Return method. See (https://github.com/tmm1/amqp/issues#issue/1).
|
28
|
+
|
29
|
+
Getting started
|
30
|
+
===============
|
31
|
+
|
32
|
+
First things first, start with:
|
33
|
+
|
34
|
+
$ gem install arvicco-amqp
|
35
|
+
|
36
|
+
To use examples with RabbitMQ, first [install the broker](http://www.rabbitmq.com/install.html).
|
37
|
+
|
38
|
+
Then have a look at the various bundled examples:
|
39
|
+
|
40
|
+
ruby examples/mq/pingpong.rb # 1-1 communication using amq.direct
|
41
|
+
ruby examples/mq/clock.rb # 1-N communication using amq.fanout
|
42
|
+
ruby examples/mq/stocks.rb # 1-subscriber communication using amq.topic
|
43
|
+
|
44
|
+
ruby examples/mq/multiclock.rb # header based routing (new rabbitmq feature)
|
45
|
+
ruby examples/mq/ack.rb # using ack
|
46
|
+
ruby examples/mq/pop.rb # pop off messages one at a time
|
47
|
+
|
48
|
+
ruby examples/mq/hashtable.rb # simple async rpc layer
|
49
|
+
ruby examples/mq/primes.rb 4 # parallelized prime number generation
|
50
|
+
ruby examples/mq/logger.rb # simple logging api
|
51
|
+
|
52
|
+
For high level API documentation see MQ class.
|
53
|
+
For more details into the lower level AMQP client API, run the simple client example:
|
54
|
+
|
55
|
+
ruby examples/amqp/simple.rb # low-level AMQP api
|
56
|
+
ruby examples/mq/internal.rb # low-level Queue/Exchange api
|
57
|
+
|
58
|
+
Or refer to protocol/doc.txt, which enumerates packets sent between a server and client
|
59
|
+
during a typical session, in both binary and decoded formats.
|
60
|
+
|
61
|
+
How to use AMQP gem with Ruby on Rails, Merb, Sinatra and other web frameworks
|
62
|
+
==============================================================================
|
63
|
+
|
64
|
+
To use AMQP gem from web applications, you would need to have EventMachine reactor running.
|
65
|
+
If you use [Thin](http://code.macournoyer.com/thin/), you are set: Thin uses EventMachine under
|
66
|
+
the hook.
|
67
|
+
|
68
|
+
With other web servers, you need to start EventMachine reactor in it's own thread like this:
|
69
|
+
|
70
|
+
Thread.new { EM.run }
|
71
|
+
|
72
|
+
because otherwise EventMachine will block current thread. Then connect to AMQP broker:
|
73
|
+
|
74
|
+
AMQP.connect(:host => "localhost", :user => "guest", :pass => "guest", :vhost => "/")
|
75
|
+
|
76
|
+
In a Ruby on Rails app, probably the best place for this code is initializer
|
77
|
+
(like config/initializers/amqp.rb). For Merb apps, it is config/init.rb. For
|
78
|
+
Sinatra and pure Rack applications, place it next to other configuration
|
79
|
+
code.
|
80
|
+
|
81
|
+
Same separate thread technique can be used to make EventMachine play nicely with other
|
82
|
+
libraries that would block current thread (like [File::Tail](http://rubygems.org/gems/file-tail)).
|
83
|
+
|
84
|
+
AMQP gem resources
|
85
|
+
==================
|
86
|
+
|
87
|
+
* [AMQP gem mailing list](http://groups.google.com/group/ruby-amqp)
|
88
|
+
* [AMQP gem at GitHub](http://github.com/tmm1/amqp)
|
89
|
+
* [AMQP gem at Rubygems](http://rubygems.org/gems/amqp)
|
90
|
+
* [This fork at GitHub](http://github.com/arvicco/amqp)
|
91
|
+
* [This fork as a gem at Rubygems](http://rubygems.org/gems/arvicco-amqp)
|
92
|
+
|
93
|
+
Running specifications suite
|
94
|
+
============================
|
95
|
+
|
96
|
+
To run the test suite make sure you have [Rspec2](http://gemcutter.org/gems/rspec) gem installed and run:
|
97
|
+
|
98
|
+
rake spec
|
99
|
+
|
100
|
+
The lib/amqp/spec.rb file is generated automatically based on the [AMQP specification](http://www.amqp.org/confluence/display/AMQP/AMQP+Specification). To generate it:
|
101
|
+
|
102
|
+
rake codegen
|
103
|
+
|
104
|
+
Writing your own AMQP/evented specs
|
105
|
+
===================================
|
106
|
+
|
107
|
+
Writing evented specs properly is a bit difficult due to gory details of event loop management. Libraries such as
|
108
|
+
[AMQP-Spec](http://github.com/arvicco/amqp-spec) and [Moqueue](https://github.com/danielsdeleo/moqueue) may be helpful.
|
109
|
+
|
110
|
+
|
111
|
+
Credits and more information
|
112
|
+
============================
|
113
|
+
|
114
|
+
(c) 2008—2010 [Aman Gupta](http://github.com/tmm1) (tmm1)
|
115
|
+
|
116
|
+
This project was inspired by [py-amqplib](http://barryp.org/software/py-amqplib/), [rabbitmq](http://rabbitmq.com), [qpid](http://qpid.apache.org/) and [rubbyt](http://github.com/rubbyt/rubbyt).
|
117
|
+
Special thanks to Dmitriy Samovskiy, Ben Hood and Tony Garnock-Jones.
|
118
|
+
|
119
|
+
AMQP brokers
|
120
|
+
------------
|
121
|
+
|
122
|
+
* [RabbitMQ](http://rabbitmq.com) (Rabbit Technologies, Erlang/OTP, MPL)
|
123
|
+
* [ZeroMQ](http://www.zeromq.org) (iMatix/FastMQ/Intel, C++, GPL3)
|
124
|
+
* [OpenAMQ](http://openamq.org) (iMatix, C, GPL2)
|
125
|
+
* [ActiveMQ](http://activemq.apache.org) (Apache Foundation, Java, Apache2)
|
126
|
+
|
127
|
+
AMQP resources
|
128
|
+
--------------
|
129
|
+
|
130
|
+
* Steve Vinoski [explains AMQP](http://steve.vinoski.net/pdf/IEEE-Advanced_Message_Queuing_Protocol.pdf) in his column, Towards Integration
|
131
|
+
|
132
|
+
* John O'Hara on [the history of AMQP](http://www.acmqueue.org/modules.php?name=Content&pa=showpage&pid=485)
|
133
|
+
|
134
|
+
* Dmitriy's [presentation on RabbitMQ/AMQP](http://somic-org.homelinux.org/blog/2008/07/31/slides-for-my-amqprabbitmq-talk/)
|
135
|
+
|
136
|
+
* ZeroMQ's [analysis of the messaging technology market](http://www.zeromq.org/whitepapers:market-analysis)
|
137
|
+
|
138
|
+
* Pieter Hintjens's [background to AMQP](http://www.openamq.org/doc:amqp-background)
|
139
|
+
|
140
|
+
* Barry Pederson's [py-amqplib](http://barryp.org/software/py-amqplib/)
|
141
|
+
|
142
|
+
* Ben Hood on [writing an AMQP client](http://hopper.squarespace.com/blog/2008/6/21/build-your-own-amqp-client.html)
|
143
|
+
|
144
|
+
* Dmitriy Samovskiy introduces [Ruby + QPid + RabbitMQ](http://somic-org.homelinux.org/blog/2008/06/24/ruby-amqp-rabbitmq-example/)
|
145
|
+
|
146
|
+
* Ben Hood's [as3-amqp](http://github.com/0x6e6562/as3-amqp) ([two](http://hopper.squarespace.com/blog/2008/7/4/server-side-as3.html), [three](http://hopper.squarespace.com/blog/2008/3/24/as3-amqp-client-first-cut.html))
|
147
|
+
|
148
|
+
* RabbitMQ's [AMQP protocol code generator](http://hg.rabbitmq.com/rabbitmq-codegen/)
|
149
|
+
|
150
|
+
* Erlang Exchange [presentation on the implementation of RabbitMQ](http://skillsmatter.com/podcast/erlang/presenting-rabbitmq-an-erlang-based-implementatio-nof-amqp) (and on the [LShift blog](http://www.lshift.net/blog/2008/07/01/slides-from-our-erlang-exchange-talk))
|
151
|
+
|
152
|
+
* Jonathan Conway's series on RabbitMQ and using it with Ruby and Merb: [One](http://jaikoo.com/2007/9/4/didn-t-you-get-the-memo), [Two](http://jaikoo.com/2008/2/29/friday-round-up-2008-02-29), [Three](http://jaikoo.com/2008/3/14/oh-hai-rabbitmq), [Four](http://jaikoo.com/2008/3/20/daemonize-rabbitmq)
|
153
|
+
|
154
|
+
* Open Enterprise's series on messaging middleware and AMQP: [Part 1](http://www1.interopsystems.com/analysis/can-amqp-break-ibms-mom-monopoly-part-1.html), [Part 2](http://www1.interopsystems.com/analysis/can-amqp-break-ibms-mom-monopoly-part-2.html), [Part 3](http://www1.interopsystems.com/analysis/can-amqp-break-ibms-mom-monopoly-part-3.html)
|
155
|
+
|
156
|
+
Messaging and distributed systems resources
|
157
|
+
-------------------------------------------
|
158
|
+
|
159
|
+
* [A Critique of the Remote Procedure Call Paradigm](http://www.cs.vu.nl/~ast/publications/euteco-1988.pdf)
|
160
|
+
|
161
|
+
* [A Note on Distributed Computing](http://research.sun.com/techrep/1994/smli_tr-94-29.pdf)
|
162
|
+
|
163
|
+
* [Convenience Over Correctness](http://steve.vinoski.net/pdf/IEEE-Convenience_Over_Correctness.pdf)
|
164
|
+
|
165
|
+
* [Metaprotocol Taxonomy and Communications Patterns](http://hessian.caucho.com/doc/metaprotocol-taxonomy.xtp)
|
166
|
+
|
167
|
+
* Joe Armstrong on [Erlang messaging vs RPC](http://armstrongonsoftware.blogspot.com/2008/05/road-we-didnt-go-down.html)
|
168
|
+
|
169
|
+
* [SEDA: scalable internet services using message queues](http://www.eecs.harvard.edu/~mdw/papers/seda-sosp01.pdf)
|
data/Rakefile
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'pathname'
|
3
|
+
|
4
|
+
BASE_PATH = Pathname.new(__FILE__).dirname
|
5
|
+
LIB_PATH = BASE_PATH + 'lib'
|
6
|
+
PKG_PATH = BASE_PATH + 'pkg'
|
7
|
+
DOC_PATH = BASE_PATH + 'rdoc'
|
8
|
+
|
9
|
+
$LOAD_PATH.unshift LIB_PATH.to_s unless $LOAD_PATH.include? LIB_PATH.to_s
|
10
|
+
|
11
|
+
require 'amqp/version'
|
12
|
+
|
13
|
+
NAME = 'ivanvanderbyl-amqp'
|
14
|
+
CLASS_NAME = AMQP
|
15
|
+
VERSION = CLASS_NAME::VERSION
|
16
|
+
|
17
|
+
# Load rakefile tasks
|
18
|
+
Dir['tasks/*.rake'].sort.each { |file| load file }
|
19
|
+
|
20
|
+
desc "Generate AMQP specification classes"
|
21
|
+
task :codegen do
|
22
|
+
sh 'ruby protocol/codegen.rb > lib/amqp/spec.rb'
|
23
|
+
sh 'ruby lib/amqp/spec.rb'
|
24
|
+
end
|
data/TODO
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
- breaks with header values that are nil (deleting nil headers in Exchange#publish for now)
|
2
|
+
|
3
|
+
- generate amqp/spec.rb from original xml spec
|
4
|
+
- add peek and pop to queues
|
5
|
+
- use rabbitmq generated consumer tag from basic.consume-ok reply
|
6
|
+
|
7
|
+
- allow temporary queues with amq.queue(nil) syntax (use uuids)
|
8
|
+
- use as temp queue in rpc
|
9
|
+
- use uuids for message ids in rpc
|
10
|
+
|
11
|
+
- add ack/completed responses for messages
|
12
|
+
- deleting queues/bindings/exchanges
|
13
|
+
+ queue.unbind
|
14
|
+
- queue.remove or queue.close or queue.delete
|
15
|
+
- exchange.remove
|
16
|
+
- rpc.remove
|
17
|
+
|
18
|
+
- handle errors and exceptions
|
19
|
+
binding to a non-existent (or not yet created in clock.rb) exchange
|
20
|
+
#<AMQP::Protocol::Channel::Close:0x11d35d4
|
21
|
+
@class_id=50,
|
22
|
+
@debug=1,
|
23
|
+
@method_id=20,
|
24
|
+
@reply_code=404,
|
25
|
+
@reply_text="NOT_FOUND - no exchange 'clock' in vhost '/'">>]
|
26
|
+
|
27
|
+
- handle connection.redirect during connect (for rabbitmq in distributed mode) [or just set insist to true]
|
28
|
+
|
29
|
+
- add amq.queue('name').size{ |num| "#{num} messages in the queue" } (send declare passive, look at declare-ok response)
|
30
|
+
- clean up MQ.default on disconnect
|
31
|
+
|
32
|
+
- add proper handling of Basic::Return (:immediate and :mandatory flags)
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.6.13.1
|
data/doc/EXAMPLE_05_ACK
ADDED
data/doc/EXAMPLE_05_POP
ADDED
data/lib/amqp.rb
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
require File.expand_path('../ext/em', __FILE__)
|
2
|
+
require File.expand_path('../ext/blankslate', __FILE__)
|
3
|
+
|
4
|
+
%w[ version buffer spec protocol frame client ].each do |file|
|
5
|
+
require File.expand_path("../amqp/#{file}", __FILE__)
|
6
|
+
end
|
7
|
+
|
8
|
+
module AMQP
|
9
|
+
class << self
|
10
|
+
@logging = false
|
11
|
+
attr_accessor :logging
|
12
|
+
attr_reader :conn, :closing
|
13
|
+
alias :closing? :closing
|
14
|
+
alias :connection :conn
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.connect *args
|
18
|
+
Client.connect *args
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.settings
|
22
|
+
@settings ||= {
|
23
|
+
# server address
|
24
|
+
:host => '127.0.0.1',
|
25
|
+
:port => PORT,
|
26
|
+
|
27
|
+
# login details
|
28
|
+
:user => 'guest',
|
29
|
+
:pass => 'guest',
|
30
|
+
:vhost => '/',
|
31
|
+
|
32
|
+
# connection timeout
|
33
|
+
:timeout => nil,
|
34
|
+
|
35
|
+
# logging
|
36
|
+
:logging => false,
|
37
|
+
|
38
|
+
# ssl
|
39
|
+
:ssl => false
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
# Must be called to startup the connection to the AMQP server.
|
44
|
+
#
|
45
|
+
# The method takes several arguments and an optional block.
|
46
|
+
#
|
47
|
+
# This takes any option that is also accepted by EventMachine::connect.
|
48
|
+
# Additionally, there are several AMQP-specific options.
|
49
|
+
#
|
50
|
+
# * :user => String (default 'guest')
|
51
|
+
# The username as defined by the AMQP server.
|
52
|
+
# * :pass => String (default 'guest')
|
53
|
+
# The password for the associated :user as defined by the AMQP server.
|
54
|
+
# * :vhost => String (default '/')
|
55
|
+
# The virtual host as defined by the AMQP server.
|
56
|
+
# * :timeout => Numeric (default nil)
|
57
|
+
# Measured in seconds.
|
58
|
+
# * :logging => true | false (default false)
|
59
|
+
# Toggle the extremely verbose logging of all protocol communications
|
60
|
+
# between the client and the server. Extremely useful for debugging.
|
61
|
+
#
|
62
|
+
# AMQP.start do
|
63
|
+
# # default is to connect to localhost:5672
|
64
|
+
#
|
65
|
+
# # define queues, exchanges and bindings here.
|
66
|
+
# # also define all subscriptions and/or publishers
|
67
|
+
# # here.
|
68
|
+
#
|
69
|
+
# # this block never exits unless EM.stop_event_loop
|
70
|
+
# # is called.
|
71
|
+
# end
|
72
|
+
#
|
73
|
+
# Most code will use the MQ api. Any calls to MQ.direct / MQ.fanout /
|
74
|
+
# MQ.topic / MQ.queue will implicitly call #start. In those cases,
|
75
|
+
# it is sufficient to put your code inside of an EventMachine.run
|
76
|
+
# block. See the code examples in MQ for details.
|
77
|
+
#
|
78
|
+
def self.start *args, &blk
|
79
|
+
EM.run{
|
80
|
+
@conn ||= connect *args
|
81
|
+
@conn.callback(&blk) if blk
|
82
|
+
@conn
|
83
|
+
}
|
84
|
+
end
|
85
|
+
|
86
|
+
class << self
|
87
|
+
alias :run :start
|
88
|
+
end
|
89
|
+
|
90
|
+
def self.stop
|
91
|
+
if @conn and not @closing
|
92
|
+
@closing = true
|
93
|
+
@conn.close{
|
94
|
+
yield if block_given?
|
95
|
+
@conn = nil
|
96
|
+
@closing = false
|
97
|
+
}
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def self.fork workers
|
102
|
+
EM.fork(workers) do
|
103
|
+
# clean up globals in the fork
|
104
|
+
Thread.current[:mq] = nil
|
105
|
+
AMQP.instance_variable_set('@conn', nil)
|
106
|
+
|
107
|
+
yield
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
data/lib/amqp/buffer.rb
ADDED
@@ -0,0 +1,270 @@
|
|
1
|
+
if [].map.respond_to? :with_index
|
2
|
+
class Array #:nodoc:
|
3
|
+
def enum_with_index
|
4
|
+
each.with_index
|
5
|
+
end
|
6
|
+
end
|
7
|
+
else
|
8
|
+
require 'enumerator'
|
9
|
+
end
|
10
|
+
|
11
|
+
module AMQP
|
12
|
+
class Buffer #:nodoc: all
|
13
|
+
class Overflow < StandardError; end
|
14
|
+
class InvalidType < StandardError; end
|
15
|
+
|
16
|
+
def initialize data = ''
|
17
|
+
@data = data
|
18
|
+
@pos = 0
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_reader :pos
|
22
|
+
|
23
|
+
def data
|
24
|
+
@data.clone
|
25
|
+
end
|
26
|
+
alias :contents :data
|
27
|
+
alias :to_s :data
|
28
|
+
|
29
|
+
def << data
|
30
|
+
@data << data.to_s
|
31
|
+
self
|
32
|
+
end
|
33
|
+
|
34
|
+
def length
|
35
|
+
@data.bytesize
|
36
|
+
end
|
37
|
+
|
38
|
+
def empty?
|
39
|
+
pos == length
|
40
|
+
end
|
41
|
+
|
42
|
+
def rewind
|
43
|
+
@pos = 0
|
44
|
+
end
|
45
|
+
|
46
|
+
def read_properties *types
|
47
|
+
types.shift if types.first == :properties
|
48
|
+
|
49
|
+
i = 0
|
50
|
+
values = []
|
51
|
+
|
52
|
+
while props = read(:short)
|
53
|
+
(0..14).each do |n|
|
54
|
+
# no more property types
|
55
|
+
break unless types[i]
|
56
|
+
|
57
|
+
# if flag is set
|
58
|
+
if props & (1<<(15-n)) != 0
|
59
|
+
if types[i] == :bit
|
60
|
+
# bit values exist in flags only
|
61
|
+
values << true
|
62
|
+
else
|
63
|
+
# save type name for later reading
|
64
|
+
values << types[i]
|
65
|
+
end
|
66
|
+
else
|
67
|
+
# property not set or is false bit
|
68
|
+
values << (types[i] == :bit ? false : nil)
|
69
|
+
end
|
70
|
+
|
71
|
+
i+=1
|
72
|
+
end
|
73
|
+
|
74
|
+
# bit(0) == 0 means no more property flags
|
75
|
+
break unless props & 1 == 1
|
76
|
+
end
|
77
|
+
|
78
|
+
values.map do |value|
|
79
|
+
value.is_a?(Symbol) ? read(value) : value
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def read *types
|
84
|
+
if types.first == :properties
|
85
|
+
return read_properties(*types)
|
86
|
+
end
|
87
|
+
|
88
|
+
values = types.map do |type|
|
89
|
+
case type
|
90
|
+
when :octet
|
91
|
+
_read(1, 'C')
|
92
|
+
when :short
|
93
|
+
_read(2, 'n')
|
94
|
+
when :long
|
95
|
+
_read(4, 'N')
|
96
|
+
when :longlong
|
97
|
+
upper, lower = _read(8, 'NN')
|
98
|
+
upper << 32 | lower
|
99
|
+
when :shortstr
|
100
|
+
_read read(:octet)
|
101
|
+
when :longstr
|
102
|
+
_read read(:long)
|
103
|
+
when :timestamp
|
104
|
+
Time.at read(:longlong)
|
105
|
+
when :table
|
106
|
+
t = Hash.new
|
107
|
+
|
108
|
+
table = Buffer.new(read(:longstr))
|
109
|
+
until table.empty?
|
110
|
+
key, type = table.read(:shortstr, :octet)
|
111
|
+
key = key.intern
|
112
|
+
t[key] ||= case type
|
113
|
+
when 83 # 'S'
|
114
|
+
table.read(:longstr)
|
115
|
+
when 73 # 'I'
|
116
|
+
table.read(:long)
|
117
|
+
when 68 # 'D'
|
118
|
+
exp = table.read(:octet)
|
119
|
+
num = table.read(:long)
|
120
|
+
num / 10.0**exp
|
121
|
+
when 84 # 'T'
|
122
|
+
table.read(:timestamp)
|
123
|
+
when 70 # 'F'
|
124
|
+
table.read(:table)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
t
|
129
|
+
when :bit
|
130
|
+
if (@bits ||= []).empty?
|
131
|
+
val = read(:octet)
|
132
|
+
@bits = (0..7).map{|i| (val & 1<<i) != 0 }
|
133
|
+
end
|
134
|
+
|
135
|
+
@bits.shift
|
136
|
+
else
|
137
|
+
raise InvalidType, "Cannot read data of type #{type}"
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
types.size == 1 ? values.first : values
|
142
|
+
end
|
143
|
+
|
144
|
+
def write type, data
|
145
|
+
case type
|
146
|
+
when :octet
|
147
|
+
_write(data, 'C')
|
148
|
+
when :short
|
149
|
+
_write(data, 'n')
|
150
|
+
when :long
|
151
|
+
_write(data, 'N')
|
152
|
+
when :longlong
|
153
|
+
lower = data & 0xffffffff
|
154
|
+
upper = (data & ~0xffffffff) >> 32
|
155
|
+
_write([upper, lower], 'NN')
|
156
|
+
when :shortstr
|
157
|
+
data = (data || '').to_s
|
158
|
+
_write([data.bytesize, data], 'Ca*')
|
159
|
+
when :longstr
|
160
|
+
if data.is_a? Hash
|
161
|
+
write(:table, data)
|
162
|
+
else
|
163
|
+
data = (data || '').to_s
|
164
|
+
_write([data.bytesize, data], 'Na*')
|
165
|
+
end
|
166
|
+
when :timestamp
|
167
|
+
write(:longlong, data.to_i)
|
168
|
+
when :table
|
169
|
+
data ||= {}
|
170
|
+
write :longstr, (data.inject(Buffer.new) do |table, (key, value)|
|
171
|
+
table.write(:shortstr, key.to_s)
|
172
|
+
|
173
|
+
case value
|
174
|
+
when String
|
175
|
+
table.write(:octet, 83) # 'S'
|
176
|
+
table.write(:longstr, value.to_s)
|
177
|
+
when Fixnum
|
178
|
+
table.write(:octet, 73) # 'I'
|
179
|
+
table.write(:long, value)
|
180
|
+
when Float
|
181
|
+
table.write(:octet, 68) # 'D'
|
182
|
+
# XXX there's gotta be a better way to do this..
|
183
|
+
exp = value.to_s.split('.').last.bytesize
|
184
|
+
num = value * 10**exp
|
185
|
+
table.write(:octet, exp)
|
186
|
+
table.write(:long, num)
|
187
|
+
when Time
|
188
|
+
table.write(:octet, 84) # 'T'
|
189
|
+
table.write(:timestamp, value)
|
190
|
+
when Hash
|
191
|
+
table.write(:octet, 70) # 'F'
|
192
|
+
table.write(:table, value)
|
193
|
+
end
|
194
|
+
|
195
|
+
table
|
196
|
+
end)
|
197
|
+
when :bit
|
198
|
+
[*data].to_enum(:each_slice, 8).each{|bits|
|
199
|
+
write(:octet, bits.enum_with_index.inject(0){ |byte, (bit, i)|
|
200
|
+
byte |= 1<<i if bit
|
201
|
+
byte
|
202
|
+
})
|
203
|
+
}
|
204
|
+
when :properties
|
205
|
+
values = []
|
206
|
+
data.enum_with_index.inject(0) do |short, ((type, value), i)|
|
207
|
+
n = i % 15
|
208
|
+
last = i+1 == data.size
|
209
|
+
|
210
|
+
if (n == 0 and i != 0) or last
|
211
|
+
if data.size > i+1
|
212
|
+
short |= 1<<0
|
213
|
+
elsif last and value
|
214
|
+
values << [type,value]
|
215
|
+
short |= 1<<(15-n)
|
216
|
+
end
|
217
|
+
|
218
|
+
write(:short, short)
|
219
|
+
short = 0
|
220
|
+
end
|
221
|
+
|
222
|
+
if value and !last
|
223
|
+
values << [type,value]
|
224
|
+
short |= 1<<(15-n)
|
225
|
+
end
|
226
|
+
|
227
|
+
short
|
228
|
+
end
|
229
|
+
|
230
|
+
values.each do |type, value|
|
231
|
+
write(type, value) unless type == :bit
|
232
|
+
end
|
233
|
+
else
|
234
|
+
raise InvalidType, "Cannot write data of type #{type}"
|
235
|
+
end
|
236
|
+
|
237
|
+
self
|
238
|
+
end
|
239
|
+
|
240
|
+
def extract
|
241
|
+
begin
|
242
|
+
cur_data, cur_pos = @data.clone, @pos
|
243
|
+
yield self
|
244
|
+
rescue Overflow
|
245
|
+
@data, @pos = cur_data, cur_pos
|
246
|
+
nil
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
def _read size, pack = nil
|
251
|
+
if @pos + size > length
|
252
|
+
raise Overflow
|
253
|
+
else
|
254
|
+
data = @data[@pos,size]
|
255
|
+
@data[@pos,size] = ''
|
256
|
+
if pack
|
257
|
+
data = data.unpack(pack)
|
258
|
+
data = data.pop if data.size == 1
|
259
|
+
end
|
260
|
+
data
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
def _write data, pack = nil
|
265
|
+
data = [*data].pack(pack) if pack
|
266
|
+
@data[@pos,0] = data
|
267
|
+
@pos += data.bytesize
|
268
|
+
end
|
269
|
+
end
|
270
|
+
end
|