iop 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2631f9835b20e87513aed68b469ca251815ccd9517c1b6879555ab851bdae02a
4
+ data.tar.gz: b1a7d1794c962323692117181c1d5485343ed38d3fec65738af5c21076801fd4
5
+ SHA512:
6
+ metadata.gz: d1cbb150db80d8de4aae4caa144618c143c84572949488508ee149b7fdcb8040385c966022a242fea08caf7533a96035fa0ee7e466cc8a583faec70b3e3c48b4
7
+ data.tar.gz: 42f468cd335d5087d4471b0f17fd190d0b7346611fe6f00da8a3a565c85042ce8a41f0a351cbe8bd40ef316311eaeae5456ec8fa79af71db3461621313435f6c
data/CHANGES.md ADDED
@@ -0,0 +1,3 @@
1
+ # 0.1.0
2
+
3
+ Initial release.
data/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # [IOP](https://bitbucket.org/fougas/iop) - the data processing pipeline construction framework for Ruby
2
+
3
+ ## Synopsis
4
+
5
+ IOP is intended for construction of the data processing pipelines in a manner of UNIX shell pipes.
6
+
7
+ Instead of the standard Ruby way of handling such I/O tasks in form of nested blocks the IOP offers a simpler flat chaining scheme.
8
+
9
+ Consider the example:
10
+
11
+ ```ruby
12
+ # One-liner example
13
+ (FileReader.new('input.dat') | GzipCompressor.new | DigestComputer.new(MD5.new) | FileWriter.new('output.dat.gz')).process!
14
+ ```
15
+
16
+ The above snippet reads input file and compresses it into the GZip-compatible output file simultaneously computing the MD5 hash of compressed data being written.
17
+
18
+ The next snippet presents the incremental pipeline construction capability - a feature not easily implementable with the standard Ruby I/O blocks nesting.
19
+
20
+ ```ruby
21
+ # Incremental pipeline construction example
22
+ pipe = FileReader.new('input')
23
+ pipe |= GzipCompressor.new if need_compression?
24
+ pipe |= FileWriter.new('output')
25
+ pipe.process!
26
+ ```
27
+
28
+ Here the GZip compression is made optional and is thrown in depending on external condition.
29
+
30
+ ## Features
31
+
32
+ The following capabilities are currently implemented:
33
+
34
+ - String splitting/merging
35
+ - IO or local file reading/writing
36
+ - FTP file reading/writing
37
+ - Digest computing
38
+ - GZip/Zlib (de)compression
39
+ - Zstd (de)compression
40
+ - Symmetric cipher (de,en)cryption
41
+ - Random data generation
42
+
43
+ ## Basic usage
44
+
45
+ - IOP is split into a set of files which should be required separately depending on which components are needed.
46
+
47
+ ```ruby
48
+ require 'iop/file'
49
+ require 'iop/zlib'
50
+ require 'iop/digest'
51
+ require 'iop/string'
52
+ ```
53
+
54
+ - The `IOP` module can be included into current namespace to conserve some writing.
55
+
56
+ ```ruby
57
+ include IOP
58
+ ```
59
+
60
+ - A chain of processing objects is created either in-line or incrementally.
61
+
62
+ ```ruby
63
+ pipe = StringSplitter.new('Greetings from IOP', 10)
64
+ pipe |= GzipCompressor.new | (digest = DigestComputer.new(MD5.new))
65
+ pipe |= FileWriter.new('output.gz')
66
+ ```
67
+
68
+ It is convenient to set local variables to the created instances which are expected to have some kind of valuable state.
69
+
70
+ - The actual processing is initiated with the `process!` method.
71
+
72
+ ```ruby
73
+ pipe.process!
74
+ ```
75
+
76
+ The IOP instances do normally perform self-cleanup operations, such as closing file handles, network connections etc., even during exception handling.
77
+
78
+ - The variable-bound instances can be then examined.
79
+
80
+ ```ruby
81
+ puts digest.hexdigest
82
+ ```
83
+
84
+ For further information refer to IOP documentation.
85
+
86
+ # The end
87
+
88
+ Cheers,
89
+
90
+ Oleg A. Khlybov <[fougas@mail.ru](mailto:fougas@mail.ru)>
data/lib/iop/digest.rb ADDED
@@ -0,0 +1,45 @@
1
+ require 'iop'
2
+ require 'digest'
3
+
4
+
5
+ module IOP
6
+
7
+
8
+ #
9
+ # Filter class to compute digest of the data being passed through.
10
+ # It can be used with digest computing classes from +digest+ and +openssl+ standard Ruby modules.
11
+ #
12
+ # ### Use case: generate 1024 bytes of random data and compute and print MD5 hash sum of it.
13
+ #
14
+ # require 'iop/digest'
15
+ # require 'iop/securerandom'
16
+ # ( IOP::SecureRandomGenerator.new(1024) | ( d = IOP::DigestComputer.new(Digest::MD5.new)) ).process!
17
+ # puts d.digest.hexdigest
18
+ #
19
+ # @since 0.1
20
+ #
21
+ class DigestComputer
22
+
23
+ include Feed
24
+ include Sink
25
+
26
+ # Returns digest object passed to constructor.
27
+ attr_reader :digest
28
+
29
+
30
+ # Creates class instance.
31
+ #
32
+ # @param digest computer instance to be fed with data
33
+ def initialize(digest)
34
+ @digest = digest
35
+ end
36
+
37
+ def process(data = nil)
38
+ digest.update(data) unless data.nil?
39
+ super
40
+ end
41
+
42
+ end
43
+
44
+
45
+ end
data/lib/iop/file.rb ADDED
@@ -0,0 +1,209 @@
1
+ require 'iop'
2
+
3
+
4
+ module IOP
5
+
6
+
7
+ #
8
+ # Feed class to read data from external +IO+ stream and send it in blocks downstream.
9
+ #
10
+ # Contrary to {FileReader}, this class does not manage attached +IO+ instance, e.g.
11
+ # it makes no attempt to close it after processing.
12
+ #
13
+ # ### Use case: sequential read of two 1024-byte blocks from the same +IO+ stream.
14
+ #
15
+ # require 'iop/file'
16
+ # require 'iop/string'
17
+ # io = File.new('input.dat', 'rb')
18
+ # begin
19
+ # ( IOP::IOReader.new(io, size: 1024) | (first = IOP::StringMerger.new) ).process!
20
+ # ( IOP::IOReader.new(io, size: 1024, offset: 1024) | (second = IOP::StringMerger.new) ).process!
21
+ # ensure
22
+ # io.close
23
+ # end
24
+ # puts first.to_s
25
+ # puts second.to_s
26
+ #
27
+ # @since 0.1
28
+ #
29
+ class IOReader
30
+
31
+ include Feed
32
+
33
+ # Creates class instance.
34
+ #
35
+ # @param io [IO] +IO+ instance to read data from
36
+ #
37
+ # @param size [Integer] total number of bytes to read; +nil+ value instructs to read until end-of-data is reached
38
+ #
39
+ # @param offset [Integer] offset in bytes from the stream start to seek to; +nil+ value means no seeking is performed
40
+ #
41
+ # @param block_size [Integer] size of blocks to read data with
42
+ def initialize(io, size: nil, offset: nil, block_size: DEFAULT_BLOCK_SIZE)
43
+ @block_size = size.nil? ? block_size : IOP.min(size, block_size)
44
+ @left = @size = size
45
+ @offset = offset
46
+ @io = io
47
+ end
48
+
49
+ def process!
50
+ @io.seek(@offset) unless @offset.nil?
51
+ data = IOP.allocate_string(@block_size)
52
+ loop do
53
+ read_size = @size.nil? ? @block_size : IOP.min(@left, @block_size)
54
+ break if read_size.zero?
55
+ if @io.read(read_size, data).nil?
56
+ if @size.nil?
57
+ break
58
+ else
59
+ raise EOFError, INSUFFICIENT_DATA
60
+ end
61
+ else
62
+ unless @left.nil?
63
+ @left -= data.size
64
+ raise IOError, EXTRA_DATA if @left < 0
65
+ end
66
+ end
67
+ process(data) unless data.size.zero?
68
+ end
69
+ process
70
+ end
71
+
72
+ end
73
+
74
+
75
+ #
76
+ # Feed class to read data from local file and send it in blocks downstream.
77
+ #
78
+ # Contrary to {IOReader}, this class manages underlying +IO+ instance in order to close it when the process is finished
79
+ # even if exception is risen.
80
+ #
81
+ # ### Use case: compute MD5 hash sum of the first 1024 bytes of a local file.
82
+ # require 'iop/file'
83
+ # require 'iop/digest'
84
+ # ( IOP::FileReader.new('input.dat', size: 1024) | (d = IOP::DigestComputer.new(Digest::MD5.new)) ).process!
85
+ # puts d.digest.hexdigest
86
+ #
87
+ # @since 0.1
88
+ #
89
+ class FileReader < IOReader
90
+
91
+ # Creates class instance.
92
+ #
93
+ # @param file [String] name of file to read from
94
+ #
95
+ # @param mode [String] open mode for the file; refer to {File} for details
96
+ #
97
+ # @param options [Hash] extra keyword parameters passed to {IOReader} constructor
98
+ def initialize(file, mode: 'rb', **options)
99
+ super(nil, **options)
100
+ @file = file
101
+ @mode = mode
102
+ end
103
+
104
+ def process!
105
+ @io = File.new(@file, @mode)
106
+ begin
107
+ super
108
+ ensure
109
+ @io.close
110
+ end
111
+ end
112
+
113
+ end
114
+
115
+
116
+ #
117
+ # Sink class to write received upstream data to external +IO+ stream.
118
+ #
119
+ # Contrary to {FileWriter}, this class does not manage attached +IO+ instance, e.g.
120
+ # it makes no attempt to close it after processing.
121
+ #
122
+ # ### Use case: concatenate two files.
123
+ #
124
+ # require 'iop/file'
125
+ # io = File.new('output.dat', 'wb')
126
+ # begin
127
+ # ( IOP::FileReader.new('file1.dat') | IOP::IOWriter.new(io) ).process!
128
+ # ( IOP::FileReader.new('file2.dat') | IOP::IOWriter.new(io) ).process!
129
+ # ensure
130
+ # io.close
131
+ # end
132
+ #
133
+ # @since 0.1
134
+ #
135
+ class IOWriter
136
+
137
+ include Sink
138
+
139
+ # Creates class instance.
140
+ #
141
+ # @param io [IO] +IO+ instance to write data to
142
+ def initialize(io)
143
+ @io = io
144
+ end
145
+
146
+ def process(data = nil)
147
+ @io.write(data)
148
+ end
149
+
150
+ end
151
+
152
+
153
+ #
154
+ # Sink class to write received upstream data to a local file.
155
+ #
156
+ # Contrary to {IOWriter}, this class manages underlying +IO+ instance in order to close it when the process is finished
157
+ # even if exception is risen.
158
+ #
159
+ # ### Use case: generate 1024 bytes of random data and write it to file.
160
+ #
161
+ # require 'iop/file'
162
+ # require 'iop/securerandom'
163
+ # ( IOP::SecureRandomGenerator.new(1024) | IOP::FileWriter.new('random.dat') ).process!
164
+ #
165
+ # @since 0.1
166
+ #
167
+ class FileWriter < IOWriter
168
+
169
+ # Creates class instance.
170
+ #
171
+ # @param file [String] name of file to write to
172
+ #
173
+ # @param mode [String] open mode for the file; refer to {File} for details
174
+ def initialize(file, mode: 'wb')
175
+ super(nil)
176
+ @file = file
177
+ @mode = mode
178
+ end
179
+
180
+ def process!
181
+ @io = File.new(@file, @mode)
182
+ begin
183
+ super
184
+ ensure
185
+ @io.close
186
+ end
187
+ end
188
+
189
+ end
190
+
191
+
192
+ # @private
193
+ class IOSegmentReader
194
+
195
+ include BufferingFeed
196
+
197
+ def initialize(io, block_size: DEFAULT_BLOCK_SIZE)
198
+ @io = io
199
+ @block_size = block_size
200
+ end
201
+
202
+ private def next_data
203
+ @io.read(@block_size)
204
+ end
205
+
206
+ end
207
+
208
+
209
+ end
@@ -0,0 +1,206 @@
1
+ require 'iop'
2
+ require 'net/ftp'
3
+
4
+
5
+ # @private
6
+ class Net::FTP
7
+ public :transfercmd, :voidresp
8
+ end
9
+
10
+
11
+ module IOP
12
+
13
+
14
+ # @private
15
+ module FTPFile
16
+
17
+ private
18
+
19
+ def setup
20
+ if @ftp.is_a?(String)
21
+ @ftp = Net::FTP.open(@ftp, @options)
22
+ @managed = true
23
+ @ftp.login
24
+ end
25
+ unless @offset.nil?
26
+ # Override resume status when offset is specified remembering current value
27
+ @resume = @ftp.resume
28
+ @ftp.resume = true
29
+ end
30
+ end
31
+
32
+ def cleanup
33
+ # Revert resume status if previously overridden
34
+ @ftp.resume = @resume unless @resume.nil?
35
+ @ftp.close if @managed
36
+ end
37
+
38
+ def transfercmd(cmd, offset = nil)
39
+ @ftp.transfercmd(cmd, offset)
40
+ end
41
+
42
+ def voidresp
43
+ @ftp.voidresp
44
+ end
45
+
46
+ end
47
+
48
+ #
49
+ # Feed class to read file from FTP server.
50
+ #
51
+ # This class an adapter for the standard Ruby +Net::FTP+ class.
52
+ #
53
+ # ### Use case: retrieve file from FTP server and store it locally.
54
+ #
55
+ # require 'iop/net/ftp'
56
+ # ( IOP::FTPFileReader.new('ftp.gnu.org', '/pub/README') | IOP::FileWriter.new('README') ).process!
57
+ #
58
+ # @since 0.1
59
+ #
60
+ class FTPFileReader
61
+
62
+ include Feed
63
+ include FTPFile
64
+
65
+ # Creates class instance.
66
+ #
67
+ # @param ftp [String, Net::FTP] FTP server to connect to
68
+ #
69
+ # @param file [String] file name to process
70
+ #
71
+ # @param size [Integer] total number of bytes to read; +nil+ value instructs to read until end-of-data is reached
72
+ #
73
+ # @param offset [Integer] offset in bytes from the stream start to seek to; +nil+ value means no seeking is performed
74
+ #
75
+ # @param block_size [Integer] size of blocks to process data with
76
+ #
77
+ # @param options [Hash] extra keyword parameters passed to +Net::FTP+ constructor
78
+ #
79
+ # _ftp_ can be either a +String+ of +Net::FTP+ instance.
80
+ # If it is a string a corresponding +Net::FTP+ instance will be created with _options_ passed to its constructor.
81
+ #
82
+ # If _ftp_ is a string, a created FTP connection is managed, e.g. it is closed after the process is complete,
83
+ # otherwise supplied object is left as is and no closing is performed.
84
+ # This allows to reuse FTP connection for a sequence of operations.
85
+ #
86
+ # Refer to +Net::FTP+ documentation for available options.
87
+ def initialize(ftp, file, size: nil, offset: nil, block_size: DEFAULT_BLOCK_SIZE, **options)
88
+ @block_size = size.nil? ? block_size : IOP.min(size, block_size)
89
+ @left = @size = size
90
+ @options = options
91
+ @offset = offset
92
+ @file = file
93
+ @ftp = ftp
94
+ end
95
+
96
+ def process!
97
+ setup
98
+ begin
99
+ # FTP logic taken from Net::FTP#retrbinary
100
+ @io = transfercmd('RETR ' << @file, @offset)
101
+ begin
102
+ loop do
103
+ read_size = @size.nil? ? @block_size : IOP.min(@left, @block_size)
104
+ break if read_size.zero?
105
+ data = @io.read(read_size)
106
+ if data.nil?
107
+ if @size.nil?
108
+ break
109
+ else
110
+ raise EOFError, INSUFFICIENT_DATA
111
+ end
112
+ else
113
+ unless @left.nil?
114
+ @left -= data.size
115
+ raise IOError, EXTRA_DATA if @left < 0
116
+ end
117
+ process(data) unless data.size.zero?
118
+ end
119
+ end
120
+ process
121
+ @io.shutdown(Socket::SHUT_WR)
122
+ @io.read_timeout = 1
123
+ @io.read
124
+ ensure
125
+ @io.close
126
+ end
127
+ voidresp
128
+ ensure
129
+ cleanup
130
+ end
131
+ end
132
+
133
+ end
134
+
135
+
136
+
137
+ #
138
+ # Sink class to write file to FTP server.
139
+ #
140
+ # This class an adapter for the standard Ruby +Net::FTP+ class.
141
+ #
142
+ # ### Use case: store a number of files filled with random data to an FTP server reusing connection.
143
+ #
144
+ # require 'iop/net/ftp'
145
+ # require 'iop/securerandom'
146
+ # ftp = Net::FTP.open('ftp.server', username: 'user')
147
+ # begin
148
+ # ftp.login
149
+ # (1..3).each do |i|
150
+ # ( IOP::SecureRandomGenerator.new(1024) | IOP::FTPFileWriter.new(ftp, "random#{i}.dat") ).process!
151
+ # end
152
+ # ensure
153
+ # ftp.close
154
+ # end
155
+ #
156
+ # @since 0.1
157
+ #
158
+ class FTPFileWriter
159
+
160
+ include Sink
161
+ include FTPFile
162
+
163
+ # Creates class instance.
164
+ #
165
+ # @param ftp [String, Net::FTP] FTP server to connect to
166
+ #
167
+ # @param file [String] file name to process
168
+ #
169
+ # @param options [Hash] extra keyword parameters passed to +Net::FTP+ constructor
170
+ #
171
+ # _ftp_ can be either a +String+ of +Net::FTP+ instance.
172
+ # If it is a string a corresponding +Net::FTP+ instance will be created with _options_ passed to its constructor.
173
+ #
174
+ # If _ftp_ is a string, a created FTP connection is managed, e.g. it is closed after the process is complete,
175
+ # otherwise supplied object is left as is and no closing is performed.
176
+ # This allows to reuse FTP connection for a sequence of operations.
177
+ def initialize(ftp, file, **options)
178
+ @options = options
179
+ @file = file
180
+ @ftp = ftp
181
+ end
182
+
183
+ def process!
184
+ setup
185
+ begin
186
+ # FTP logic taken from Net::FTP#storbinary
187
+ @io = transfercmd('STOR ' << @file)
188
+ begin
189
+ super
190
+ ensure
191
+ @io.close
192
+ end
193
+ voidresp
194
+ ensure
195
+ cleanup
196
+ end
197
+ end
198
+
199
+ def process(data = nil)
200
+ @io.write(data) unless data.nil?
201
+ end
202
+
203
+ end
204
+
205
+
206
+ end
@@ -0,0 +1,147 @@
1
+ require 'iop'
2
+ require 'openssl'
3
+
4
+
5
+ module IOP
6
+
7
+
8
+ # Default cipher ID for OpenSSL adapters.
9
+ DEFAULT_OPENSSL_CIPHER = 'AES-256-CBC'.freeze
10
+
11
+
12
+ #
13
+ # Filter class to perform encryption with a symmetric key algorithm (ciphering) of the data passed through.
14
+ #
15
+ # The class is an adapter for +OpenSSL::Cipher+ & compatible classes.
16
+ #
17
+ # ### Use case: generate 1024 bytes of random data encrypt is with default cipher algorithm and generated key & initial vector.
18
+ #
19
+ # require 'iop/openssl'
20
+ # require 'iop/securerandom'
21
+ # ( IOP::SecureRandomGenerator.new(1024) | (c = IOP::CipherEncryptor.new) ).process!
22
+ # puts c.key
23
+ #
24
+ # @since 0.1
25
+ #
26
+ class CipherEncryptor
27
+
28
+ include Feed
29
+ include Sink
30
+
31
+ # Returns initial vector (IV) for encryption session.
32
+ attr_reader :iv
33
+
34
+ # Returns encryption key.
35
+ attr_reader :key
36
+
37
+ # Creates class instance.
38
+ #
39
+ # @param cipher [String, OpenSSL::Cipher] cipher used for encryption
40
+ #
41
+ # @param key [String] string representing an encryption key or +nil+
42
+ #
43
+ # @param iv [String] string representing an initial vector or +nil+
44
+ #
45
+ # _cipher_ can be either a +String+ or +OpenSSL::Cipher+ instance.
46
+ # If it is a string, a corresponding +OpenSSL::Cipher+ instance will be created.
47
+ #
48
+ # If _key_ is +nil+, a new key will be generated in secure manner which can be accessed later with {#key} method.
49
+ #
50
+ # If _iv_ is +nil+, a new initial vector will be generated in secure manner which can be accessed later with {#iv} method.
51
+ # If _iv_ is +nil+ the generated initial vector will be injected into the downstream data preceding the encrypted data itself.
52
+ #
53
+ # Note that key and initial vector are both cipher-dependent. Refer to +OpenSSL::Cipher+ documentation for more information.
54
+ def initialize(cipher = DEFAULT_OPENSSL_CIPHER, key: nil, iv: nil)
55
+ @cipher = cipher.is_a?(String) ? OpenSSL::Cipher.new(cipher) : cipher
56
+ @cipher.encrypt
57
+ @key = key.nil? ? @cipher.random_key : @cipher.key = key
58
+ @iv = if iv.nil?
59
+ @embed_iv = true
60
+ @cipher.random_iv
61
+ else
62
+ @cipher.iv = iv
63
+ end
64
+ end
65
+
66
+ def process(data = nil)
67
+ unless @continue
68
+ @continue = true
69
+ super(iv) if @embed_iv
70
+ @buffer = IOP.allocate_string(data.size)
71
+ end
72
+ if data.nil?
73
+ super(@cipher.final)
74
+ super
75
+ else
76
+ super(@cipher.update(data, @buffer)) unless data.size.zero?
77
+ end
78
+ end
79
+
80
+ end
81
+
82
+
83
+ #
84
+ # Filter class to perform decryption with a symmetric key algorithm (ciphering) of the data passed through.
85
+ #
86
+ # The class is an adapter for +OpenSSL::Cipher+ & compatible classes.
87
+ #
88
+ # ### Use case: decrypt a file with default algorithm and embedded initial vector.
89
+ #
90
+ # require 'iop/file'
91
+ # require 'iop/openssl'
92
+ # ( IOP::FileReader.new('input.aes') | IOP::CipherDecryptor.new(key: my_secret_key) | (s = IOP::StringMerger.new) ).process!
93
+ # puts s.to_s
94
+ #
95
+ # @since 0.1
96
+ #
97
+ class CipherDecryptor
98
+
99
+ include Feed
100
+ include Sink
101
+
102
+ # Returns initial vector (IV) for decryption session.
103
+ attr_reader :iv
104
+
105
+ # Returns decryption key.
106
+ attr_reader :key
107
+
108
+ # Creates class instance.
109
+ #
110
+ # @param cipher [String, OpenSSL::Cipher] cipher used for decryption
111
+ #
112
+ # @param key [String] string representing an encryption key
113
+ #
114
+ # @param iv [String] string representing an initial vector or +nil+
115
+ #
116
+ # _cipher_ can be either a +String+ or +OpenSSL::Cipher+ instance.
117
+ # If it is a string, a corresponding +OpenSSL::Cipher+ instance will be created.
118
+ #
119
+ # If _iv_ is +nil+, the initial vector will be obtained from the upstream data. Refer to {CipherEncryptor#initialize} for details.
120
+ def initialize(cipher = DEFAULT_OPENSSL_CIPHER, key:, iv: nil)
121
+ @cipher = cipher.is_a?(String) ? OpenSSL::Cipher.new(cipher) : cipher
122
+ @cipher.decrypt
123
+ @cipher.key = @key = key
124
+ @cipher.iv = @iv = iv unless iv.nil?
125
+ end
126
+
127
+ def process(data = nil)
128
+ unless @continue
129
+ @continue = true
130
+ @buffer = IOP.allocate_string(data.size)
131
+ if iv.nil?
132
+ @cipher.iv = @iv = data[0, @cipher.iv_len]
133
+ data = data[@cipher.iv_len..-1]
134
+ end
135
+ end
136
+ if data.nil?
137
+ super(@cipher.final)
138
+ super
139
+ else
140
+ super(@cipher.update(data, @buffer)) unless data.size.zero?
141
+ end
142
+ end
143
+
144
+ end
145
+
146
+
147
+ end