cztop 0.1.1 → 0.2.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.
- checksums.yaml +4 -4
- data/CHANGES.md +15 -0
- data/README.md +5 -0
- data/exe/z85decode +31 -0
- data/exe/z85encode +31 -0
- data/lib/cztop.rb +2 -0
- data/lib/cztop/poller.rb +58 -56
- data/lib/cztop/socket/types.rb +4 -1
- data/lib/cztop/version.rb +1 -1
- data/lib/cztop/z85.rb +34 -90
- data/lib/cztop/z85/padded.rb +98 -0
- data/lib/cztop/z85/pipe.rb +178 -0
- metadata +9 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 38df565064ee6d04ad3eeaaf668e68574cb8e0cc
|
4
|
+
data.tar.gz: 62eb8513064a9546d4c04c57411314ffb8bdc162
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 028dbf962ccf069d64048d12107fcc322e66a5553c34e2c8081e15319e602eac13c88ca903a0290bb9fbd6dda22c398ea8e4e16d99fa01f0fab6cf049110df13
|
7
|
+
data.tar.gz: 7f6f6cdf10066b9a8ae4c26f5b3d0025da04a1a6aeb0caec8bc6ab550a16955057162543ac83e0ec0fd2535923637c5beafb6acf1a6247228492f97531862dc4
|
data/CHANGES.md
CHANGED
@@ -1,3 +1,18 @@
|
|
1
|
+
0.2.0 (xx/01/2016)
|
2
|
+
-----
|
3
|
+
* simplify CZTop::Z85::Padded
|
4
|
+
* no length encoding
|
5
|
+
* padding similar to PKCS#7
|
6
|
+
* add utilities `z85encode` and `z85decode`
|
7
|
+
* add CZTop::Z85::Pipe
|
8
|
+
* CZTop::SUB#subscribe: subscribe to everything if no parameter given
|
9
|
+
|
10
|
+
0.1.1 (23/01/2016)
|
11
|
+
-----
|
12
|
+
* add support for Ruby 2.0
|
13
|
+
* improve documentation
|
14
|
+
* fix require()s in examples
|
15
|
+
|
1
16
|
0.1.0 (23/01/2016)
|
2
17
|
-----
|
3
18
|
* first release
|
data/README.md
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
[](http://inch-ci.org/github/paddor/cztop)
|
4
4
|
[](https://gemnasium.com/paddor/cztop)
|
5
5
|
[](https://coveralls.io/github/paddor/cztop?branch=master)
|
6
|
+
[](LICENSE)
|
6
7
|
|
7
8
|
# CZTop
|
8
9
|
|
@@ -389,6 +390,10 @@ $
|
|
389
390
|
* cannot handle CLIENT/SERVER sockets
|
390
391
|
* there good timer gems for Ruby
|
391
392
|
* Poller can be used to embed in an existing event loop (Celluloid), or make your own trivial one
|
393
|
+
* [x] provide `z85encode` and `z85decode` utilities
|
394
|
+
* can be used in a pipeline (limited memory usage)
|
395
|
+
* reusable interface: `Z85::Pipe`
|
396
|
+
|
392
397
|
|
393
398
|
## Contributing
|
394
399
|
|
data/exe/z85decode
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'cztop'
|
3
|
+
require 'optparse'
|
4
|
+
require 'benchmark'
|
5
|
+
|
6
|
+
options = { strategy: CZTop::Z85::Pipe::Strategy::Sequential }
|
7
|
+
OptionParser.new do |opts|
|
8
|
+
opts.banner = "Usage: #$0 [options]"
|
9
|
+
|
10
|
+
opts.on("-p", "--parallel", "read, decode, and write in parallel " +
|
11
|
+
"using 3 threads") do
|
12
|
+
options[:strategy] = CZTop::Z85::Pipe::Strategy::Parallel
|
13
|
+
end
|
14
|
+
opts.on("-v", "--verbose", "print some statistics afterwards") do
|
15
|
+
options[:verbose] = true
|
16
|
+
end
|
17
|
+
end.parse!
|
18
|
+
|
19
|
+
pipe = CZTop::Z85::Pipe.new(STDIN, STDOUT, strategy: options[:strategy])
|
20
|
+
bytes_decoded = nil
|
21
|
+
tms = Benchmark.measure { bytes_decoded = pipe.decode }
|
22
|
+
exit unless options[:verbose]
|
23
|
+
|
24
|
+
if tms.real < 0.1
|
25
|
+
warn "#{$0}: decoding took %.2f us." % (tms.real * 1_000_000)
|
26
|
+
else
|
27
|
+
warn "#{$0}: decoding took %.3f seconds." % tms.real
|
28
|
+
end
|
29
|
+
|
30
|
+
throughput = (bytes_decoded * 8 / tms.real) / 1_000_000
|
31
|
+
warn "#{$0}: mean throughput: %.3f [Mb/s]" % throughput
|
data/exe/z85encode
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'cztop'
|
3
|
+
require 'optparse'
|
4
|
+
require 'benchmark'
|
5
|
+
|
6
|
+
options = { strategy: CZTop::Z85::Pipe::Strategy::Sequential }
|
7
|
+
OptionParser.new do |opts|
|
8
|
+
opts.banner = "Usage: #$0 [options]"
|
9
|
+
|
10
|
+
opts.on("-p", "--parallel", "read, encode, and write in parallel " +
|
11
|
+
"using 3 threads") do
|
12
|
+
options[:strategy] = CZTop::Z85::Pipe::Strategy::Parallel
|
13
|
+
end
|
14
|
+
opts.on("-v", "--verbose", "print some statistics afterwards") do
|
15
|
+
options[:verbose] = true
|
16
|
+
end
|
17
|
+
end.parse!
|
18
|
+
|
19
|
+
pipe = CZTop::Z85::Pipe.new(STDIN, STDOUT, strategy: options[:strategy])
|
20
|
+
bytes_encoded = nil
|
21
|
+
tms = Benchmark.measure { bytes_encoded = pipe.encode }
|
22
|
+
exit unless options[:verbose]
|
23
|
+
|
24
|
+
if tms.real < 0.1
|
25
|
+
warn "#{$0}: encoding took %.2f us." % (tms.real * 1_000_000)
|
26
|
+
else
|
27
|
+
warn "#{$0}: encoding took %.3f seconds." % tms.real
|
28
|
+
end
|
29
|
+
|
30
|
+
throughput = (bytes_encoded * 8 / tms.real) / 1_000_000
|
31
|
+
warn "#{$0}: mean throughput: %.3f [Mb/s]" % throughput
|
data/lib/cztop.rb
CHANGED
@@ -31,6 +31,8 @@ require_relative 'cztop/config/traversing'
|
|
31
31
|
require_relative 'cztop/config/serialization'
|
32
32
|
require_relative 'cztop/message/frames'
|
33
33
|
require_relative 'cztop/socket/types'
|
34
|
+
require_relative 'cztop/z85/padded'
|
35
|
+
require_relative 'cztop/z85/pipe'
|
34
36
|
|
35
37
|
|
36
38
|
# make Ctrl-C work in case a low-level call hangs
|
data/lib/cztop/poller.rb
CHANGED
@@ -30,57 +30,7 @@ module CZTop
|
|
30
30
|
# ugly.
|
31
31
|
#
|
32
32
|
class Poller
|
33
|
-
|
34
|
-
module ZMQ
|
35
|
-
|
36
|
-
POLL = 1
|
37
|
-
POLLIN = 1
|
38
|
-
POLLOUT = 2
|
39
|
-
POLLERR = 4
|
40
|
-
|
41
|
-
extend ::FFI::Library
|
42
|
-
lib_name = 'libzmq'
|
43
|
-
lib_paths = ['/usr/local/lib', '/opt/local/lib', '/usr/lib64']
|
44
|
-
.map { |path| "#{path}/#{lib_name}.#{::FFI::Platform::LIBSUFFIX}" }
|
45
|
-
ffi_lib lib_paths + [lib_name]
|
46
|
-
|
47
|
-
# Represents a struct of type +zmq_pollitem_t+.
|
48
|
-
class PollItem < FFI::Struct
|
49
|
-
##
|
50
|
-
# shamelessly taken from https://github.com/mtortonesi/ruby-czmq-ffi
|
51
|
-
#
|
52
|
-
|
53
|
-
|
54
|
-
FD_TYPE = if FFI::Platform::IS_WINDOWS && FFI::Platform::ADDRESS_SIZE == 64
|
55
|
-
# On Windows, zmq.h defines fd as a SOCKET, which is 64 bits on x64.
|
56
|
-
:uint64
|
57
|
-
else
|
58
|
-
:int
|
59
|
-
end
|
60
|
-
|
61
|
-
layout :socket, :pointer,
|
62
|
-
:fd, FD_TYPE,
|
63
|
-
:events, :short,
|
64
|
-
:revents, :short
|
65
|
-
|
66
|
-
# @return [Boolean] whether the socket is readable
|
67
|
-
def readable?
|
68
|
-
(self[:revents] & POLLIN) > 0
|
69
|
-
end
|
70
|
-
|
71
|
-
# @return [Boolean] whether the socket is writable
|
72
|
-
def writable?
|
73
|
-
(self[:revents] & POLLOUT) > 0
|
74
|
-
end
|
75
|
-
end
|
76
|
-
|
77
|
-
opts = {
|
78
|
-
blocking: true # only necessary on MRI to deal with the GIL.
|
79
|
-
}
|
80
|
-
|
81
|
-
#ZMQ_EXPORT int zmq_poll (zmq_pollitem_t *items, int nitems, long timeout);
|
82
|
-
attach_function :poll, :zmq_poll, [:pointer, :int, :long], :int, **opts
|
83
|
-
end
|
33
|
+
include ::CZMQ::FFI
|
84
34
|
|
85
35
|
# @param readers [Socket, Actor] sockets to poll for input
|
86
36
|
def initialize(*readers)
|
@@ -108,7 +58,7 @@ module CZTop
|
|
108
58
|
# @raise [ArgumentError] if it's not a socket
|
109
59
|
def add_reader(socket)
|
110
60
|
raise ArgumentError unless socket.is_a?(Socket) || socket.is_a?(Actor)
|
111
|
-
ptr =
|
61
|
+
ptr = Zsock.resolve(socket) # get low-level handle
|
112
62
|
@readers[ptr.to_i] = socket
|
113
63
|
@rebuild_needed = true
|
114
64
|
end
|
@@ -120,7 +70,7 @@ module CZTop
|
|
120
70
|
# @raise [ArgumentError] if it's not a socket
|
121
71
|
def remove_reader(socket)
|
122
72
|
raise ArgumentError unless socket.is_a?(Socket) || socket.is_a?(Actor)
|
123
|
-
ptr =
|
73
|
+
ptr = Zsock.resolve(socket) # get low-level handle
|
124
74
|
@readers.delete(ptr.to_i) and @rebuild_needed = true
|
125
75
|
end
|
126
76
|
|
@@ -130,7 +80,7 @@ module CZTop
|
|
130
80
|
# @raise [ArgumentError] if it's not a socket
|
131
81
|
def add_writer(socket)
|
132
82
|
raise ArgumentError unless socket.is_a?(Socket) || socket.is_a?(Actor)
|
133
|
-
ptr =
|
83
|
+
ptr = Zsock.resolve(socket) # get low-level handle
|
134
84
|
@writers[ptr.to_i] = socket
|
135
85
|
@rebuild_needed = true
|
136
86
|
end
|
@@ -142,7 +92,7 @@ module CZTop
|
|
142
92
|
# @raise [ArgumentError] if it's not a socket
|
143
93
|
def remove_writer(socket)
|
144
94
|
raise ArgumentError unless socket.is_a?(Socket) || socket.is_a?(Actor)
|
145
|
-
ptr =
|
95
|
+
ptr = Zsock.resolve(socket) # get low-level handle
|
146
96
|
@writers.delete(ptr.to_i) and @rebuild_needed = true
|
147
97
|
end
|
148
98
|
|
@@ -212,13 +162,65 @@ module CZTop
|
|
212
162
|
# @return [ZMQ::PollItem] a new item for
|
213
163
|
def new_item(address, socket, events)
|
214
164
|
item = ZMQ::PollItem.new(address)
|
215
|
-
item[:socket] =
|
165
|
+
item[:socket] = Zsock.resolve(socket)
|
216
166
|
item[:fd] = 0
|
217
167
|
item[:events] = events
|
218
168
|
item[:revents] = 0
|
219
169
|
item
|
220
170
|
end
|
221
171
|
|
172
|
+
# CZTop's interface to the low-level +zmq_poll()+ function.
|
173
|
+
module ZMQ
|
174
|
+
|
175
|
+
POLL = 1
|
176
|
+
POLLIN = 1
|
177
|
+
POLLOUT = 2
|
178
|
+
POLLERR = 4
|
179
|
+
|
180
|
+
extend ::FFI::Library
|
181
|
+
lib_name = 'libzmq'
|
182
|
+
lib_paths = ['/usr/local/lib', '/opt/local/lib', '/usr/lib64']
|
183
|
+
.map { |path| "#{path}/#{lib_name}.#{::FFI::Platform::LIBSUFFIX}" }
|
184
|
+
ffi_lib lib_paths + [lib_name]
|
185
|
+
|
186
|
+
# Represents a struct of type +zmq_pollitem_t+.
|
187
|
+
class PollItem < FFI::Struct
|
188
|
+
##
|
189
|
+
# shamelessly taken from https://github.com/mtortonesi/ruby-czmq-ffi
|
190
|
+
#
|
191
|
+
|
192
|
+
|
193
|
+
FD_TYPE = if FFI::Platform::IS_WINDOWS && FFI::Platform::ADDRESS_SIZE == 64
|
194
|
+
# On Windows, zmq.h defines fd as a SOCKET, which is 64 bits on x64.
|
195
|
+
:uint64
|
196
|
+
else
|
197
|
+
:int
|
198
|
+
end
|
199
|
+
|
200
|
+
layout :socket, :pointer,
|
201
|
+
:fd, FD_TYPE,
|
202
|
+
:events, :short,
|
203
|
+
:revents, :short
|
204
|
+
|
205
|
+
# @return [Boolean] whether the socket is readable
|
206
|
+
def readable?
|
207
|
+
(self[:revents] & POLLIN) > 0
|
208
|
+
end
|
209
|
+
|
210
|
+
# @return [Boolean] whether the socket is writable
|
211
|
+
def writable?
|
212
|
+
(self[:revents] & POLLOUT) > 0
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
opts = {
|
217
|
+
blocking: true # only necessary on MRI to deal with the GIL.
|
218
|
+
}
|
219
|
+
|
220
|
+
#ZMQ_EXPORT int zmq_poll (zmq_pollitem_t *items, int nitems, long timeout);
|
221
|
+
attach_function :poll, :zmq_poll, [:pointer, :int, :long], :int, **opts
|
222
|
+
end
|
223
|
+
|
222
224
|
# This is the trivial poller based on zpoller. It only supports polling
|
223
225
|
# for reading, but it also supports doing that on CLIENT/SERVER sockets,
|
224
226
|
# which is useful for {CZTop::Poller}.
|
data/lib/cztop/socket/types.rb
CHANGED
@@ -134,10 +134,13 @@ module CZTop
|
|
134
134
|
attach_ffi_delegate(Zsock.new_sub(endpoints, subscription))
|
135
135
|
end
|
136
136
|
|
137
|
+
# @return [String] subscription prefix to subscribe to everything
|
138
|
+
EVERYTHING = ""
|
139
|
+
|
137
140
|
# Subscribes to the given prefix string.
|
138
141
|
# @param prefix [String] prefix string to subscribe to
|
139
142
|
# @return [void]
|
140
|
-
def subscribe(prefix)
|
143
|
+
def subscribe(prefix = EVERYTHING)
|
141
144
|
ffi_delegate.set_subscribe(prefix)
|
142
145
|
end
|
143
146
|
|
data/lib/cztop/version.rb
CHANGED
data/lib/cztop/z85.rb
CHANGED
@@ -7,6 +7,40 @@ module CZTop
|
|
7
7
|
include HasFFIDelegate
|
8
8
|
extend CZTop::HasFFIDelegate::ClassMethods
|
9
9
|
|
10
|
+
class << self
|
11
|
+
# Same as {Z85#encode}, but without the need to create an instance
|
12
|
+
# first.
|
13
|
+
#
|
14
|
+
# @param input [String] possibly binary input data
|
15
|
+
# @return [String] Z85 encoded data as ASCII string
|
16
|
+
# @raise [ArgumentError] if input length isn't divisible by 4 with no
|
17
|
+
# remainder
|
18
|
+
# @raise [SystemCallError] if this fails
|
19
|
+
def encode(input)
|
20
|
+
default.encode(input)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Same as {Z85#decode}, but without the need to create an instance
|
24
|
+
# first.
|
25
|
+
#
|
26
|
+
# @param input [String] Z85 encoded data
|
27
|
+
# @return [String] original data as binary string
|
28
|
+
# @raise [ArgumentError] if input length isn't divisible by 5 with no
|
29
|
+
# remainder
|
30
|
+
# @raise [SystemCallError] if this fails
|
31
|
+
def decode(input)
|
32
|
+
default.decode(input)
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
# Default instance of {Z85}.
|
38
|
+
# @return [Z85] memoized default instance
|
39
|
+
def default
|
40
|
+
@default ||= Z85.new
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
10
44
|
def initialize
|
11
45
|
attach_ffi_delegate(CZMQ::FFI::Zarmour.new)
|
12
46
|
ffi_delegate.set_mode(:mode_z85)
|
@@ -63,95 +97,5 @@ module CZTop
|
|
63
97
|
size_ptr.read_uint32
|
64
98
|
end
|
65
99
|
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
100
|
end
|
157
101
|
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# Z85 with simple padding. This allows you to {#encode} input of any
|
2
|
+
# length.
|
3
|
+
#
|
4
|
+
# = Padding Scheme
|
5
|
+
#
|
6
|
+
# If the data to be encoded is empty (0 bytes), it is encoded to the empty
|
7
|
+
# string, just like in Z85.
|
8
|
+
#
|
9
|
+
# Otherwise, a small padding sequence of 1 to 4 (identical) bytes is
|
10
|
+
# appended to the binary data. Its last byte denotes the number of padding
|
11
|
+
# bytes. This padding is done even if the length of the binary input is
|
12
|
+
# a multiple of 4 bytes. This is similar to PKCS#7 padding.
|
13
|
+
#
|
14
|
+
# +----------------------------------------+------------+
|
15
|
+
# | binary data | padding |
|
16
|
+
# | any number of bytes | 1-4 bytes |
|
17
|
+
# +----------------------------------------+------------+
|
18
|
+
#
|
19
|
+
# The resulting blob is encoded using {CZTop::Z85#encode}.
|
20
|
+
#
|
21
|
+
# When decoding, {CZTop::Z85#decode} does the inverse. After decoding, it
|
22
|
+
# checks the last byte to determine the number of bytes of padding used,
|
23
|
+
# and chops those off.
|
24
|
+
#
|
25
|
+
# @note Warning: Z85 doesn't have a standardized padding procedure. So
|
26
|
+
# other implementations won't automatically recognize and chop off the
|
27
|
+
# padding. Only use this if you really need padding, like when you can't
|
28
|
+
# guarantee the input for {#encode} is always a multiple of 4 bytes.
|
29
|
+
#
|
30
|
+
# @see https://en.wikipedia.org/wiki/Padding_(cryptography)#PKCS7
|
31
|
+
class CZTop::Z85::Padded < CZTop::Z85
|
32
|
+
class << self
|
33
|
+
# Same as {Z85::Padded#encode}, but without the need to create an
|
34
|
+
# instance first.
|
35
|
+
#
|
36
|
+
# @param input [String] possibly binary input data
|
37
|
+
# @return [String] Z85 encoded data as ASCII string, including encoded
|
38
|
+
# length and padding
|
39
|
+
# @raise [SystemCallError] if this fails
|
40
|
+
def encode(input)
|
41
|
+
default.encode(input)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Same as {Z85::Padded#decode}, but without the need to create an
|
45
|
+
# instance first.
|
46
|
+
#
|
47
|
+
# @param input [String] Z85 encoded data (including padding, or empty
|
48
|
+
# string)
|
49
|
+
# @return [String] original data as binary string
|
50
|
+
# @raise [SystemCallError] if this fails
|
51
|
+
def decode(input)
|
52
|
+
default.decode(input)
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
# Default instance of {Z85::Padded}.
|
58
|
+
# @return [Z85::Padded] memoized default instance
|
59
|
+
def default
|
60
|
+
@default ||= CZTop::Z85::Padded.new
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Encododes to Z85, with padding if needed.
|
65
|
+
#
|
66
|
+
# @param input [String] possibly binary input data
|
67
|
+
# @return [String] Z85 encoded data as ASCII string, including padding
|
68
|
+
# @raise [SystemCallError] if this fails
|
69
|
+
def encode(input)
|
70
|
+
return super if input.empty?
|
71
|
+
|
72
|
+
padding_bytes = 4 - (input.bytesize % 4)
|
73
|
+
|
74
|
+
# if 0, make it 4. we MUST append padding.
|
75
|
+
padding_bytes = 4 if padding_bytes == 0
|
76
|
+
|
77
|
+
# generate and append padding
|
78
|
+
padding = [padding_bytes].pack("C") * padding_bytes
|
79
|
+
|
80
|
+
super("#{input}#{padding}")
|
81
|
+
end
|
82
|
+
|
83
|
+
# Decodes from Z85 with padding.
|
84
|
+
#
|
85
|
+
# @param input [String] Z85 encoded data (including padding, or empty
|
86
|
+
# string)
|
87
|
+
# @return [String] original data as binary string
|
88
|
+
# @raise [SystemCallError] if this fails
|
89
|
+
def decode(input)
|
90
|
+
return super if input.empty?
|
91
|
+
decoded = super
|
92
|
+
|
93
|
+
# last byte contains number of padding bytes
|
94
|
+
padding_bytes = decoded.byteslice(-1).ord
|
95
|
+
|
96
|
+
decoded.byteslice( 0 ... -padding_bytes )
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,178 @@
|
|
1
|
+
# Can be used if you want to encode or decode data from one IO to another.
|
2
|
+
# It'll do so until it hits EOF in the source IO.
|
3
|
+
class CZTop::Z85::Pipe
|
4
|
+
# @param source [IO] where to read data
|
5
|
+
# @param sink [IO] where to write data
|
6
|
+
# @param strategy [Strategy] algorithm to use (pass the class itself,
|
7
|
+
# not an instance)
|
8
|
+
def initialize(source, sink, strategy: Strategy::Parallel)
|
9
|
+
@source, @sink = source, sink
|
10
|
+
@strategy = strategy
|
11
|
+
@bin_bytes = 0 # processed binary data (non-Z85)
|
12
|
+
end
|
13
|
+
|
14
|
+
# @return [Integer] size of chunks read when encoding
|
15
|
+
ENCODE_READ_SZ = (32 * 2**10) # 32 KiB (for full chunks read)
|
16
|
+
|
17
|
+
# @return [Integer] size of chunks read when decoding
|
18
|
+
DECODE_READ_SZ = ENCODE_READ_SZ / 4 * 5
|
19
|
+
|
20
|
+
# Encodes data from source and writes Z85 data to sink. This is done
|
21
|
+
# until EOF is hit on the source.
|
22
|
+
#
|
23
|
+
# @return [Integer] number of bytes read (binary data)
|
24
|
+
def encode
|
25
|
+
padded = false
|
26
|
+
@strategy.new(@source, @sink, ENCODE_READ_SZ) do |chunk, prev_chunk|
|
27
|
+
@bin_bytes += chunk.bytesize if chunk
|
28
|
+
if prev_chunk && chunk
|
29
|
+
CZTop::Z85.encode(prev_chunk)
|
30
|
+
elsif prev_chunk && chunk.nil? # last chunk
|
31
|
+
CZTop::Z85::Padded.encode(prev_chunk)
|
32
|
+
elsif prev_chunk.nil? && chunk.nil?
|
33
|
+
CZTop::Z85.encode("") # empty input
|
34
|
+
else
|
35
|
+
"" # very first chunk. don't encode anything yet...
|
36
|
+
end
|
37
|
+
end.execute
|
38
|
+
return @bin_bytes
|
39
|
+
end
|
40
|
+
|
41
|
+
# Decodes Z85 data from source and writes decoded data to sink. This is
|
42
|
+
# done until EOF is hit on the source.
|
43
|
+
#
|
44
|
+
# @return [Integer] number of bytes written (binary data)
|
45
|
+
def decode
|
46
|
+
@strategy.new(@source, @sink, DECODE_READ_SZ) do |chunk, prev_chunk|
|
47
|
+
if prev_chunk && chunk
|
48
|
+
CZTop::Z85.decode(prev_chunk)
|
49
|
+
elsif prev_chunk && chunk.nil?
|
50
|
+
CZTop::Z85::Padded.decode(prev_chunk)
|
51
|
+
elsif prev_chunk.nil? && chunk.nil?
|
52
|
+
CZTop::Z85.decode("") # empty input
|
53
|
+
else
|
54
|
+
"" # very first chunk. don't decode anything yet...
|
55
|
+
end
|
56
|
+
end.execute
|
57
|
+
return @bin_bytes
|
58
|
+
end
|
59
|
+
|
60
|
+
# @abstract
|
61
|
+
# Different encoding/decoding strategies (algorithms).
|
62
|
+
#
|
63
|
+
# This is mainly just for me to practice the GoF Strategy Pattern.
|
64
|
+
class Strategy
|
65
|
+
# @param source [IO] the source
|
66
|
+
# @param sink [IO] the sink
|
67
|
+
# @param read_sz [Integer] chunk size when reading from source
|
68
|
+
# @param xcode [Proc] block to encode or decode data
|
69
|
+
# @yieldparam chunk [String, nil] current chunk (or +nil+ after the
|
70
|
+
# last one)
|
71
|
+
# @yieldparam prev_chunk [String, nil] previous chunk (or +nil+ for
|
72
|
+
# the first time)
|
73
|
+
# @yieldreturn [String] encoded/decoded chunk to write to sink
|
74
|
+
def initialize(source, sink, read_sz, &xcode)
|
75
|
+
@source = source
|
76
|
+
@sink = sink
|
77
|
+
@read_sz = read_sz
|
78
|
+
@xcode = xcode
|
79
|
+
end
|
80
|
+
|
81
|
+
# @abstract
|
82
|
+
# Runs the algorithm.
|
83
|
+
# @raise [void]
|
84
|
+
def execute() raise NotImplementedError end
|
85
|
+
|
86
|
+
# A single thread that is either reading input, encoding/decoding, or
|
87
|
+
# writing output.
|
88
|
+
class Sequential < Strategy
|
89
|
+
# Runs the algorithm.
|
90
|
+
# @raise [void]
|
91
|
+
def execute
|
92
|
+
previous_chunk = nil
|
93
|
+
while true
|
94
|
+
chunk = @source.read(@read_sz)
|
95
|
+
@sink << @xcode.(chunk, previous_chunk)
|
96
|
+
break if chunk.nil?
|
97
|
+
previous_chunk = chunk
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Uses three threads:
|
103
|
+
#
|
104
|
+
# 1. reads from source
|
105
|
+
# 2. encodes/decodes
|
106
|
+
# 3. thread that writes to sink
|
107
|
+
#
|
108
|
+
# This might give a performance increase on truly parallel
|
109
|
+
# platforms such as Rubinius and JRuby (and multiple CPU cores).
|
110
|
+
#
|
111
|
+
class Parallel < Strategy
|
112
|
+
# Initializes the 2 sized queues used.
|
113
|
+
def initialize(*)
|
114
|
+
super
|
115
|
+
# @source
|
116
|
+
# |
|
117
|
+
# V
|
118
|
+
@source_queue = SizedQueue.new(20) # limit memory usage
|
119
|
+
# |
|
120
|
+
# V
|
121
|
+
# xcode
|
122
|
+
# |
|
123
|
+
# V
|
124
|
+
@sink_queue = SizedQueue.new(20) # limit memory usage
|
125
|
+
# |
|
126
|
+
# V
|
127
|
+
# @sink
|
128
|
+
end
|
129
|
+
|
130
|
+
# Runs the algorithm.
|
131
|
+
# @raise [void]
|
132
|
+
def execute
|
133
|
+
Thread.new { read }
|
134
|
+
Thread.new { xcode }
|
135
|
+
write
|
136
|
+
end
|
137
|
+
|
138
|
+
private
|
139
|
+
|
140
|
+
# Reads all chunks and pushes them into the source queue. Then
|
141
|
+
# pushes a +nil+ into the queue.
|
142
|
+
# @return [void]
|
143
|
+
def read
|
144
|
+
while chunk = @source.read(@read_sz)
|
145
|
+
@source_queue << chunk
|
146
|
+
end
|
147
|
+
@source_queue << nil
|
148
|
+
end
|
149
|
+
|
150
|
+
# Pops all chunks from the source queue, encodes or decodes them,
|
151
|
+
# and pushes the result into the sink queue. Then pushes a +nil+
|
152
|
+
# into the queue.
|
153
|
+
# @return [void]
|
154
|
+
def xcode
|
155
|
+
# Encode all but the last chunk with pure Z85.
|
156
|
+
previous_chunk = nil
|
157
|
+
while true
|
158
|
+
chunk = @source_queue.pop
|
159
|
+
|
160
|
+
# call @xcode for the trailing nil-chunk as well
|
161
|
+
@sink_queue << @xcode.(chunk, previous_chunk)
|
162
|
+
|
163
|
+
break if chunk.nil?
|
164
|
+
previous_chunk = chunk
|
165
|
+
end
|
166
|
+
@sink_queue << nil
|
167
|
+
end
|
168
|
+
|
169
|
+
# Pops all chunks from the sink queue and writes them to the sink.
|
170
|
+
# @return [void]
|
171
|
+
def write
|
172
|
+
while chunk = @sink_queue.pop
|
173
|
+
@sink << chunk
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cztop
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Patrik Wenger
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-01-
|
11
|
+
date: 2016-01-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: czmq-ffi-gen
|
@@ -209,7 +209,9 @@ dependencies:
|
|
209
209
|
description:
|
210
210
|
email:
|
211
211
|
- paddor@gmail.com
|
212
|
-
executables:
|
212
|
+
executables:
|
213
|
+
- z85decode
|
214
|
+
- z85encode
|
213
215
|
extensions: []
|
214
216
|
extra_rdoc_files: []
|
215
217
|
files:
|
@@ -241,6 +243,8 @@ files:
|
|
241
243
|
- examples/taxi_system/generate_keys.rb
|
242
244
|
- examples/taxi_system/start_broker.sh
|
243
245
|
- examples/taxi_system/start_clients.sh
|
246
|
+
- exe/z85decode
|
247
|
+
- exe/z85encode
|
244
248
|
- lib/cztop.rb
|
245
249
|
- lib/cztop/actor.rb
|
246
250
|
- lib/cztop/authenticator.rb
|
@@ -263,6 +267,8 @@ files:
|
|
263
267
|
- lib/cztop/socket/types.rb
|
264
268
|
- lib/cztop/version.rb
|
265
269
|
- lib/cztop/z85.rb
|
270
|
+
- lib/cztop/z85/padded.rb
|
271
|
+
- lib/cztop/z85/pipe.rb
|
266
272
|
- lib/cztop/zsock_options.rb
|
267
273
|
- perf/README.md
|
268
274
|
- perf/inproc_lat.rb
|