cztop 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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