iop 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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