cztop 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +31 -0
  5. data/.yardopts +1 -0
  6. data/AUTHORS +1 -0
  7. data/CHANGES.md +3 -0
  8. data/Gemfile +10 -0
  9. data/Guardfile +61 -0
  10. data/LICENSE +5 -0
  11. data/Procfile +3 -0
  12. data/README.md +408 -0
  13. data/Rakefile +6 -0
  14. data/bin/console +7 -0
  15. data/bin/setup +7 -0
  16. data/ci-scripts/install-deps +9 -0
  17. data/cztop.gemspec +36 -0
  18. data/examples/ruby_actor/actor.rb +100 -0
  19. data/examples/simple_req_rep/rep.rb +12 -0
  20. data/examples/simple_req_rep/req.rb +35 -0
  21. data/examples/taxi_system/.gitignore +2 -0
  22. data/examples/taxi_system/Makefile +2 -0
  23. data/examples/taxi_system/README.gsl +115 -0
  24. data/examples/taxi_system/README.md +276 -0
  25. data/examples/taxi_system/broker.rb +98 -0
  26. data/examples/taxi_system/client.rb +34 -0
  27. data/examples/taxi_system/generate_keys.rb +24 -0
  28. data/examples/taxi_system/start_broker.sh +2 -0
  29. data/examples/taxi_system/start_clients.sh +11 -0
  30. data/lib/cztop/actor.rb +308 -0
  31. data/lib/cztop/authenticator.rb +97 -0
  32. data/lib/cztop/beacon.rb +96 -0
  33. data/lib/cztop/certificate.rb +176 -0
  34. data/lib/cztop/config/comments.rb +66 -0
  35. data/lib/cztop/config/serialization.rb +82 -0
  36. data/lib/cztop/config/traversing.rb +157 -0
  37. data/lib/cztop/config.rb +119 -0
  38. data/lib/cztop/frame.rb +158 -0
  39. data/lib/cztop/has_ffi_delegate.rb +85 -0
  40. data/lib/cztop/message/frames.rb +74 -0
  41. data/lib/cztop/message.rb +191 -0
  42. data/lib/cztop/monitor.rb +102 -0
  43. data/lib/cztop/poller.rb +334 -0
  44. data/lib/cztop/polymorphic_zsock_methods.rb +24 -0
  45. data/lib/cztop/proxy.rb +149 -0
  46. data/lib/cztop/send_receive_methods.rb +35 -0
  47. data/lib/cztop/socket/types.rb +207 -0
  48. data/lib/cztop/socket.rb +106 -0
  49. data/lib/cztop/version.rb +3 -0
  50. data/lib/cztop/z85.rb +157 -0
  51. data/lib/cztop/zsock_options.rb +334 -0
  52. data/lib/cztop.rb +55 -0
  53. data/perf/README.md +79 -0
  54. data/perf/inproc_lat.rb +49 -0
  55. data/perf/inproc_thru.rb +42 -0
  56. data/perf/local_lat.rb +35 -0
  57. data/perf/remote_lat.rb +26 -0
  58. metadata +297 -0
@@ -0,0 +1,24 @@
1
+ module CZTop
2
+
3
+ # These are methods that can be used on a {Socket} as well as an {Actor}.
4
+ # @see http://api.zeromq.org/czmq3-0:zsock
5
+ module PolymorphicZsockMethods
6
+ # Sends a signal.
7
+ # @param status [Integer] signal (0-255)
8
+ def signal(status = 0)
9
+ ::CZMQ::FFI::Zsock.signal(ffi_delegate, status)
10
+ end
11
+
12
+ # Waits for a signal.
13
+ # @return [Integer] the received signal
14
+ def wait
15
+ ::CZMQ::FFI::Zsock.wait(ffi_delegate)
16
+ end
17
+
18
+ # Set socket to use unbounded pipes (HWM=0); use this in cases when you are
19
+ # totally certain the message volume can fit in memory.
20
+ def set_unbounded
21
+ ::CZMQ::FFI::Zsock.set_unbounded(ffi_delegate)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,149 @@
1
+ module CZTop
2
+ # Steerable proxy which switches messages between a frontend and a backend
3
+ # socket.
4
+ #
5
+ # This is implemented using an {Actor}.
6
+ #
7
+ # @see http://api.zeromq.org/czmq3-0:zproxy
8
+ class Proxy
9
+ include ::CZMQ::FFI
10
+
11
+ # function pointer to the `zmonitor()` function
12
+ ZPROXY_FPTR = ::CZMQ::FFI.ffi_libraries.each do |dl|
13
+ fptr = dl.find_function("zproxy")
14
+ break fptr if fptr
15
+ end
16
+ raise LoadError, "couldn't find zproxy()" if ZPROXY_FPTR.nil?
17
+
18
+ def initialize
19
+ @actor = Actor.new(ZPROXY_FPTR)
20
+ end
21
+
22
+ # @return [Actor] the actor behind this proxy
23
+ attr_reader :actor
24
+
25
+ # Terminates the proxy.
26
+ # @return [void]
27
+ def terminate
28
+ @actor.terminate
29
+ end
30
+
31
+ # Enable verbose logging of commands and activity.
32
+ # @return [void]
33
+ def verbose!
34
+ @actor << "VERBOSE"
35
+ @actor.wait
36
+ end
37
+
38
+ # Returns a configurator object which you can use to configure the
39
+ # frontend socket.
40
+ # @return [Configurator] (memoized) frontend configurator
41
+ def frontend
42
+ @frontend ||= Configurator.new(self, :frontend)
43
+ end
44
+
45
+ # Returns a configurator object which you can use to configure the backend
46
+ # socket.
47
+ # @return [Configurator] (memoized) backend configurator
48
+ def backend
49
+ @backend ||= Configurator.new(self, :backend)
50
+ end
51
+
52
+ # Captures all proxied messages and delivers them to a PULL socket bound
53
+ # to the specified endpoint.
54
+ # @note The PULL socket has to be bound before calling this method.
55
+ # @param endpoint [String] the endpoint to which the PULL socket is bound to
56
+ # @return [void]
57
+ def capture(endpoint)
58
+ @actor << ["CAPTURE", endpoint]
59
+ @actor.wait
60
+ end
61
+
62
+ # Pauses proxying of any messages.
63
+ # @note This causes any messages to be queued up and potentialy hit the
64
+ # high-water mark on the frontend or backend socket, causing messages to
65
+ # be dropped or writing applications to block.
66
+ # @return [void]
67
+ def pause
68
+ @actor << "PAUSE"
69
+ @actor.wait
70
+ end
71
+
72
+ # Resume proxying of messages.
73
+ # @note This is only needed after a call to {#pause}, not to start the
74
+ # proxy. Proxying starts as soon as the frontend and backend sockets are
75
+ # properly attached.
76
+ # @return [void]
77
+ def resume
78
+ @actor << "RESUME"
79
+ @actor.wait
80
+ end
81
+
82
+ # Used to configure the socket on one side of a {Proxy}.
83
+ class Configurator
84
+ # @return [Array<Symbol>] supported socket types
85
+ SOCKET_TYPES = %i[
86
+ PAIR PUB SUB REQ REP
87
+ DEALER ROUTER PULL PUSH
88
+ XPUB XSUB
89
+ ]
90
+
91
+ # @param proxy [Proxy] the proxy instance
92
+ # @param side [Symbol] :frontend or :backend
93
+ def initialize(proxy, side)
94
+ @proxy = proxy
95
+ @side = case side
96
+ when :frontend then "FRONTEND"
97
+ when :backend then "BACKEND"
98
+ else raise ArgumentError, "invalid side: #{side.inspect}"
99
+ end
100
+ end
101
+
102
+ # @return [Proxy] the proxy this {Configurator} works on
103
+ attr_reader :proxy
104
+
105
+ # @return [String] the side, either "FRONTEND" or "BACKEND"
106
+ attr_reader :side
107
+
108
+ # Creates and binds a serverish socket.
109
+ # @param socket_type [Symbol] one of {SOCKET_TYPES}
110
+ # @param endpoint [String] endpoint to bind to
111
+ # @raise [ArgumentError] if the given socket type is invalid
112
+ # @return [void]
113
+ def bind(socket_type, endpoint)
114
+ unless SOCKET_TYPES.include?(socket_type)
115
+ raise ArgumentError, "invalid socket type: #{socket_type}"
116
+ end
117
+ @proxy.actor << [ @side, socket_type.to_s, endpoint ]
118
+ @proxy.actor.wait
119
+ end
120
+
121
+ # Set ZAP domain for authentication.
122
+ # @param domain [String] the ZAP domain
123
+ def domain=(domain)
124
+ @proxy.actor << [ "DOMAIN", @side, domain ]
125
+ @proxy.actor.wait
126
+ end
127
+
128
+ # Configure PLAIN authentication on this socket.
129
+ # @note You'll have to use a {CZTop::Authenticator}.
130
+ def PLAIN_server!
131
+ @proxy.actor << [ "PLAIN", @side ]
132
+ @proxy.actor.wait
133
+ end
134
+
135
+ # Configure CURVE authentication on this socket.
136
+ # @note You'll have to use a {CZTop::Authenticator}.
137
+ # @param cert [Certificate] this server's certificate,
138
+ # so remote clients are able to authenticate this server
139
+ def CURVE_server!(cert)
140
+ public_key = cert.public_key
141
+ secret_key = cert.secret_key or
142
+ raise ArgumentError, "no secret key in certificate"
143
+
144
+ @proxy.actor << [ "CURVE", @side, public_key, secret_key ]
145
+ @proxy.actor.wait
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,35 @@
1
+ module CZTop
2
+
3
+ # These are methods that can be used on a {Socket} as well as an {Actor},
4
+ # but actually just pass through to methods of {Message} (which take
5
+ # a polymorphic reference, in Ruby as well as in C).
6
+ # @see http://api.zeromq.org/czmq3-0:zmsg
7
+ module SendReceiveMethods
8
+ # Sends a message.
9
+ #
10
+ # @param message [Message, String, Array<parts>] the message to send
11
+ # @raise [IO::EAGAINWaitWritable] if send timeout has been reached (see
12
+ # {ZsockOptions::OptionsAccessor#sndtimeo=})
13
+ # @raise [Interrupt, ArgumentError, SystemCallError] anything raised by
14
+ # {Message#send_to}
15
+ # @return [self]
16
+ # @see Message.coerce
17
+ # @see Message#send_to
18
+ def <<(message)
19
+ Message.coerce(message).send_to(self)
20
+ self
21
+ end
22
+
23
+ # Receives a message.
24
+ #
25
+ # @return [Message]
26
+ # @raise [IO::EAGAINWaitReadable] if receive timeout has been reached (see
27
+ # {ZsockOptions::OptionsAccessor#rcvtimeo=})
28
+ # @raise [Interrupt, ArgumentError, SystemCallError] anything raised by
29
+ # {Message.receive_from}
30
+ # @see Message.receive_from
31
+ def receive
32
+ Message.receive_from(self)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,207 @@
1
+ module CZTop
2
+ class Socket
3
+ # Socket types. Each constant in this namespace holds the type code used
4
+ # for the zsock_new() function.
5
+ module Types
6
+ PAIR = 0
7
+ PUB = 1
8
+ SUB = 2
9
+ REQ = 3
10
+ REP = 4
11
+ DEALER = 5
12
+ ROUTER = 6
13
+ PULL = 7
14
+ PUSH = 8
15
+ XPUB = 9
16
+ XSUB = 10
17
+ STREAM = 11
18
+ SERVER = 12
19
+ CLIENT = 13
20
+ end
21
+
22
+ # All the available type codes, mapped to their Symbol equivalent.
23
+ # @return [Hash<Integer, Symbol>]
24
+ TypeNames = Hash[
25
+ Types.constants.map { |name| i = Types.const_get(name); [ i, name ] }
26
+ ].freeze
27
+
28
+ # @param type [Symbol, Integer] type from {Types} or like +:PUB+
29
+ # @return [REQ, REP, PUSH, PULL, ... ] the new socket
30
+ # @see Types
31
+ # @example Creating a socket by providing its type as a parameter
32
+ # my_sock = CZTop::Socket.new_by_type(:DEALER, "tcp://example.com:4000")
33
+ def self.new_by_type(type)
34
+ case type
35
+ when Integer
36
+ type_code = type
37
+ type_name = TypeNames[type_code] or
38
+ raise ArgumentError, "invalid type %p" % type
39
+ type_class = Socket.const_get(type_name)
40
+ when Symbol
41
+ type_code = Types.const_get(type)
42
+ type_class = Socket.const_get(type)
43
+ else
44
+ raise ArgumentError, "invalid socket type: %p" % type
45
+ end
46
+ ffi_delegate = Zsock.new(type_code)
47
+ sock = type_class.allocate
48
+ sock.attach_ffi_delegate(ffi_delegate)
49
+ sock
50
+ end
51
+
52
+ # Client socket for the ZeroMQ Client-Server Pattern.
53
+ # @see http://rfc.zeromq.org/spec:41
54
+ class CLIENT < Socket
55
+ # @param endpoints [String] endpoints to connect to
56
+ def initialize(endpoints = nil)
57
+ attach_ffi_delegate(Zsock.new_client(endpoints))
58
+ end
59
+ end
60
+
61
+ # Server socket for the ZeroMQ Client-Server Pattern.
62
+ # @see http://rfc.zeromq.org/spec:41
63
+ class SERVER < Socket
64
+ # @param endpoints [String] endpoints to bind to
65
+ def initialize(endpoints = nil)
66
+ attach_ffi_delegate(Zsock.new_server(endpoints))
67
+ end
68
+ end
69
+
70
+ # Request socket for the ZeroMQ Request-Reply Pattern.
71
+ # @see http://rfc.zeromq.org/spec:28
72
+ class REQ < Socket
73
+ # @param endpoints [String] endpoints to connect to
74
+ def initialize(endpoints = nil)
75
+ attach_ffi_delegate(Zsock.new_req(endpoints))
76
+ end
77
+ end
78
+
79
+ # Reply socket for the ZeroMQ Request-Reply Pattern.
80
+ # @see http://rfc.zeromq.org/spec:28
81
+ class REP < Socket
82
+ # @param endpoints [String] endpoints to bind to
83
+ def initialize(endpoints = nil)
84
+ attach_ffi_delegate(Zsock.new_rep(endpoints))
85
+ end
86
+ end
87
+
88
+ # Dealer socket for the ZeroMQ Request-Reply Pattern.
89
+ # @see http://rfc.zeromq.org/spec:28
90
+ class DEALER < Socket
91
+ # @param endpoints [String] endpoints to connect to
92
+ def initialize(endpoints = nil)
93
+ attach_ffi_delegate(Zsock.new_dealer(endpoints))
94
+ end
95
+ end
96
+
97
+ # Router socket for the ZeroMQ Request-Reply Pattern.
98
+ # @see http://rfc.zeromq.org/spec:28
99
+ class ROUTER < Socket
100
+ # @param endpoints [String] endpoints to bind to
101
+ def initialize(endpoints = nil)
102
+ attach_ffi_delegate(Zsock.new_router(endpoints))
103
+ end
104
+
105
+ # Send a message to a specific receiver. This is a shorthand for when
106
+ # you send a message to a specific receiver with no hops in between.
107
+ # @param receiver [String] receiving peer's socket identity
108
+ # @param message [Message] the message to send
109
+ # @note Do NOT use the message afterwards. It'll have been modified and
110
+ # destroyed.
111
+ def send_to(receiver, message)
112
+ message = Message.coerce(message)
113
+ message.prepend "" # separator frame
114
+ message.prepend receiver # receiver envelope
115
+ self << message
116
+ end
117
+ end
118
+
119
+ # Publish socket for the ZeroMQ Publish-Subscribe Pattern.
120
+ # @see http://rfc.zeromq.org/spec:29
121
+ class PUB < Socket
122
+ # @param endpoints [String] endpoints to bind to
123
+ def initialize(endpoints = nil)
124
+ attach_ffi_delegate(Zsock.new_pub(endpoints))
125
+ end
126
+ end
127
+
128
+ # Subscribe socket for the ZeroMQ Publish-Subscribe Pattern.
129
+ # @see http://rfc.zeromq.org/spec:29
130
+ class SUB < Socket
131
+ # @param endpoints [String] endpoints to connect to
132
+ # @param subscription [String] what to subscribe to
133
+ def initialize(endpoints = nil, subscription = nil)
134
+ attach_ffi_delegate(Zsock.new_sub(endpoints, subscription))
135
+ end
136
+
137
+ # Subscribes to the given prefix string.
138
+ # @param prefix [String] prefix string to subscribe to
139
+ # @return [void]
140
+ def subscribe(prefix)
141
+ ffi_delegate.set_subscribe(prefix)
142
+ end
143
+
144
+ # Unsubscribes from the given prefix.
145
+ # @param prefix [String] prefix string to unsubscribe from
146
+ # @return [void]
147
+ def unsubscribe(prefix)
148
+ ffi_delegate.set_unsubscribe(prefix)
149
+ end
150
+ end
151
+
152
+ # Extended publish socket for the ZeroMQ Publish-Subscribe Pattern.
153
+ # @see http://rfc.zeromq.org/spec:29
154
+ class XPUB < Socket
155
+ # @param endpoints [String] endpoints to bind to
156
+ def initialize(endpoints = nil)
157
+ attach_ffi_delegate(Zsock.new_xpub(endpoints))
158
+ end
159
+ end
160
+
161
+ # Extended subscribe socket for the ZeroMQ Publish-Subscribe Pattern.
162
+ # @see http://rfc.zeromq.org/spec:29
163
+ class XSUB < Socket
164
+ # @param endpoints [String] endpoints to connect to
165
+ def initialize(endpoints = nil)
166
+ attach_ffi_delegate(Zsock.new_xsub(endpoints))
167
+ end
168
+ end
169
+
170
+ # Push socket for the ZeroMQ Pipeline Pattern.
171
+ # @see http://rfc.zeromq.org/spec:30
172
+ class PUSH < Socket
173
+ # @param endpoints [String] endpoints to connect to
174
+ def initialize(endpoints = nil)
175
+ attach_ffi_delegate(Zsock.new_push(endpoints))
176
+ end
177
+ end
178
+
179
+ # Pull socket for the ZeroMQ Pipeline Pattern.
180
+ # @see http://rfc.zeromq.org/spec:30
181
+ class PULL < Socket
182
+ # @param endpoints [String] endpoints to bind to
183
+ def initialize(endpoints = nil)
184
+ attach_ffi_delegate(Zsock.new_pull(endpoints))
185
+ end
186
+ end
187
+
188
+ # Pair socket for inter-thread communication.
189
+ # @see http://rfc.zeromq.org/spec:31
190
+ class PAIR < Socket
191
+ # @param endpoints [String] endpoints to connect to
192
+ def initialize(endpoints = nil)
193
+ attach_ffi_delegate(Zsock.new_pair(endpoints))
194
+ end
195
+ end
196
+
197
+ # Stream socket for the native pattern over. This is useful when
198
+ # communicating with a non-ZMQ peer, done over TCP.
199
+ # @see http://api.zeromq.org/4-2:zmq-socket#toc16
200
+ class STREAM < Socket
201
+ # @param endpoints [String] endpoints to connect to
202
+ def initialize(endpoints = nil)
203
+ attach_ffi_delegate(Zsock.new_stream(endpoints))
204
+ end
205
+ end
206
+ end
207
+ end
@@ -0,0 +1,106 @@
1
+ module CZTop
2
+ # Represents a CZMQ::FFI::Zsock.
3
+ class Socket
4
+ include HasFFIDelegate
5
+ extend CZTop::HasFFIDelegate::ClassMethods
6
+ include ZsockOptions
7
+ include SendReceiveMethods
8
+ include PolymorphicZsockMethods
9
+ include CZMQ::FFI
10
+
11
+ # @!group CURVE Security
12
+
13
+ # Enables CURVE security and makes this socket a CURVE server.
14
+ # @param cert [Certificate] this server's certificate,
15
+ # so remote clients are able to authenticate this server
16
+ # @note You'll have to use a {CZTop::Authenticator}.
17
+ # @return [void]
18
+ def CURVE_server!(cert)
19
+ options.CURVE_server = true
20
+ cert.apply(self) # NOTE: desired: raises if no secret key in cert
21
+ end
22
+
23
+ # Enables CURVE security and makes this socket a CURVE client.
24
+ # @param client_cert [Certificate] client's certificate, to secure
25
+ # communication (and be authenticated by the server)
26
+ # @param server_cert [Certificate] the remote server's certificate, so
27
+ # this socket is able to authenticate the server
28
+ # @return [void]
29
+ # @raise [SecurityError] if the server's secret key is set in server_cert,
30
+ # which means it's not secret anymore
31
+ # @raise [SystemCallError] if there's no secret key in client_cert
32
+ def CURVE_client!(client_cert, server_cert)
33
+ if server_cert.secret_key
34
+ raise SecurityError, "server's secret key not secret"
35
+ end
36
+
37
+ client_cert.apply(self) # NOTE: desired: raises if no secret key in cert
38
+ options.CURVE_serverkey = server_cert.public_key
39
+ end
40
+
41
+ # @!endgroup
42
+
43
+ # @return [String] last bound endpoint, if any
44
+ def last_endpoint
45
+ ffi_delegate.endpoint
46
+ end
47
+
48
+ # Connects to an endpoint.
49
+ # @param endpoint [String]
50
+ # @return [void]
51
+ # @raise [ArgumentError] if the endpoint is incorrect
52
+ def connect(endpoint)
53
+ rc = ffi_delegate.connect("%s", :string, endpoint)
54
+ raise ArgumentError, "incorrect endpoint: %p" % endpoint if rc == -1
55
+ end
56
+
57
+ # Disconnects from an endpoint.
58
+ # @param endpoint [String]
59
+ # @raise [ArgumentError] if the endpoint is incorrect
60
+ def disconnect(endpoint)
61
+ rc = ffi_delegate.disconnect("%s", :string, endpoint)
62
+ raise ArgumentError, "incorrect endpoint: %p" % endpoint if rc == -1
63
+ end
64
+
65
+ # Closes and destroys the native socket.
66
+ # @note Don't try to use it anymore afterwards.
67
+ def close
68
+ ffi_delegate.destroy
69
+ end
70
+
71
+ # @return [Integer] last automatically selected, bound TCP port, if any
72
+ # @return [nil] if not bound to a TCP port yet
73
+ attr_reader :last_tcp_port
74
+
75
+ # Binds to an endpoint.
76
+ # @note When binding to an automatically selected TCP port, this will set
77
+ # {#last_tcp_port}.
78
+ # @param endpoint [String]
79
+ # @return [void]
80
+ # @raise [SystemCallError] in case of failure
81
+ def bind(endpoint)
82
+ rc = ffi_delegate.bind("%s", :string, endpoint)
83
+ raise_zmq_err("unable to bind to %p" % endpoint) if rc == -1
84
+ @last_tcp_port = rc if rc > 0
85
+ end
86
+
87
+ # Unbinds from an endpoint.
88
+ # @param endpoint [String]
89
+ # @return [void]
90
+ # @raise [ArgumentError] if the endpoint is incorrect
91
+ def unbind(endpoint)
92
+ rc = ffi_delegate.unbind("%s", :string, endpoint)
93
+ raise ArgumentError, "incorrect endpoint: %p" % endpoint if rc == -1
94
+ end
95
+
96
+ # Inspects this {Socket}.
97
+ # @return [String] shows class, native address, and {#last_endpoint}
98
+ def inspect
99
+ "#<%s:0x%x last_endpoint=%p>" % [
100
+ self.class,
101
+ to_ptr.address,
102
+ last_endpoint
103
+ ]
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,3 @@
1
+ module CZTop
2
+ VERSION = "0.1.0"
3
+ end
data/lib/cztop/z85.rb ADDED
@@ -0,0 +1,157 @@
1
+ module CZTop
2
+ # Represents a CZMQ::FFI::Zarmour in Z85 mode.
3
+ #
4
+ # Use this class to encode to and from the Z85 encoding algorithm.
5
+ # @see http://rfc.zeromq.org/spec:32
6
+ class Z85
7
+ include HasFFIDelegate
8
+ extend CZTop::HasFFIDelegate::ClassMethods
9
+
10
+ def initialize
11
+ attach_ffi_delegate(CZMQ::FFI::Zarmour.new)
12
+ ffi_delegate.set_mode(:mode_z85)
13
+ end
14
+
15
+ # Encodes to Z85.
16
+ # @param input [String] possibly binary input data
17
+ # @return [String] Z85 encoded data as ASCII string
18
+ # @raise [ArgumentError] if input length isn't divisible by 4 with no
19
+ # remainder
20
+ # @raise [SystemCallError] if this fails
21
+ def encode(input)
22
+ raise ArgumentError, "wrong input length" if input.bytesize % 4 > 0
23
+ input = input.dup.force_encoding(Encoding::BINARY)
24
+ ptr = ffi_delegate.encode(input, input.bytesize)
25
+ raise_zmq_err if ptr.null?
26
+ z85 = ptr.read_string
27
+ z85.encode!(Encoding::ASCII)
28
+ return z85
29
+ end
30
+
31
+ # Decodes from Z85.
32
+ # @param input [String] Z85 encoded data
33
+ # @return [String] original data as binary string
34
+ # @raise [ArgumentError] if input length isn't divisible by 5 with no
35
+ # remainder
36
+ # @raise [SystemCallError] if this fails
37
+ def decode(input)
38
+ raise ArgumentError, "wrong input length" if input.bytesize % 5 > 0
39
+ FFI::MemoryPointer.new(:size_t) do |size_ptr|
40
+ buffer_ptr = ffi_delegate.decode(input, size_ptr)
41
+ raise_zmq_err if buffer_ptr.null?
42
+ decoded_string = buffer_ptr.read_string(_size(size_ptr) - 1)
43
+ return decoded_string
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ # Gets correct size, depending on the platform.
50
+ # @return [Integer]
51
+ # @see https://github.com/ffi/ffi/issues/398
52
+ # @see https://github.com/ffi/ffi/issues/333
53
+ def _size(size_ptr)
54
+ if RUBY_ENGINE == "jruby"
55
+ # NOTE: JRuby FFI doesn't have #read_uint64, nor does it have
56
+ # Pointer::SIZE
57
+ return size_ptr.read_ulong_long
58
+ end
59
+
60
+ if ::FFI::Pointer::SIZE == 8 # 64 bit
61
+ size_ptr.read_uint64
62
+ else
63
+ size_ptr.read_uint32
64
+ end
65
+ end
66
+
67
+ # Z85 with simple padding. This allows you to {#encode} input of any
68
+ # length.
69
+ #
70
+ # = Encoding Procedure
71
+ #
72
+ # If the data to be encoded is empty (0 bytes), it is encoded to the empty
73
+ # string, just like in Z85.
74
+ #
75
+ # Otherwise, a length information is prepended and, if needed, padding (1,
76
+ # 2, or 3 NULL bytes) is appended to bring the resulting blob to
77
+ # a multiple of 4 bytes.
78
+ #
79
+ # The length information is encoded similarly to lengths of messages
80
+ # (frames) in ZMTP. Up to 127 bytes, the data's length is encoded with
81
+ # a single byte (specifically, with the 7 least significant bits in it).
82
+ #
83
+ # +--------+-------------------------------+------------+
84
+ # | length | data | padding |
85
+ # | 1 byte | up to 127 bytes | 0-3 bytes |
86
+ # +--------+-------------------------------+------------+
87
+ #
88
+ # If the data is 128 bytes or more, the most significant bit will be set
89
+ # to indicate that fact, and a 64 bit unsigned integer in network byte
90
+ # order is appended after this first byte to encode the length of the
91
+ # data. This means that up to 16EiB (exbibytes) can be encoded, which
92
+ # will be enough for the foreseeable future.
93
+ #
94
+ # +--------+-----------+----------------------------------+------------+
95
+ # | big? | length | data | padding |
96
+ # | 1 byte | 8 bytes | 128 bytes or much more | 0-3 bytes |
97
+ # +--------+-----------+----------------------------------+------------+
98
+ #
99
+ # The resulting blob is encoded using {CZTop::Z85#encode}.
100
+ # {CZTop::Z85#decode} does the inverse.
101
+ #
102
+ # @note Warning: This won't be compatible with other implementations of
103
+ # Z85. Only use this if you really need padding, like when you can't
104
+ # guarantee the input for {#encode} is always a multiple of 4 bytes.
105
+ #
106
+ class Padded < Z85
107
+ # Encododes to Z85, with padding if needed.
108
+ #
109
+ # If input isn't empty, 8 additional bytes for the encoded length will
110
+ # be prepended. If needed, 1 to 3 bytes of padding will be appended.
111
+ #
112
+ # If input is empty, returns the empty string.
113
+ #
114
+ # @param input [String] possibly binary input data
115
+ # @return [String] Z85 encoded data as ASCII string, including encoded
116
+ # length and padding
117
+ # @raise [SystemCallError] if this fails
118
+ def encode(input)
119
+ return super if input.empty?
120
+ length = input.bytesize
121
+ if length < 1<<7 # up to 127 bytes
122
+ encoded_length = [length].pack("C")
123
+
124
+ else # larger input
125
+ low = length & 0xFFFFFFFF
126
+ high = (length >> 32) & 0xFFFFFFFF
127
+ encoded_length = [ 1<<7, high, low ].pack("CNN")
128
+ end
129
+ padding = "\0" * ((4 - ((length+1) % 4)) % 4)
130
+ super("#{encoded_length}#{input}#{padding}")
131
+ end
132
+
133
+ # Decodes from Z85 with padding.
134
+ #
135
+ # @param input [String] Z85 encoded data (including encoded length and
136
+ # padding, or empty string)
137
+ # @return [String] original data as binary string
138
+ # @raise [ArgumentError] if input is invalid or truncated
139
+ # @raise [SystemCallError] if this fails
140
+ def decode(input)
141
+ return super if input.empty?
142
+ decoded = super
143
+ length = decoded.byteslice(0, 1).unpack("C")[0]
144
+ if (1<<7 & length).zero? # up to 127 bytes
145
+ decoded = decoded.byteslice(1, length) # extract payload
146
+
147
+ else # larger input
148
+ length = decoded.byteslice(1, 8).unpack("NN")
149
+ .inject(0) { |sum, i| (sum << 32) + i }
150
+ decoded = decoded.byteslice(9, length) # extract payload
151
+ end
152
+ raise ArgumentError, "input truncated" if decoded.bytesize < length
153
+ return decoded
154
+ end
155
+ end
156
+ end
157
+ end