iostreams 1.2.1 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +6 -3
- data/lib/io_streams/builder.rb +9 -6
- data/lib/io_streams/deprecated.rb +1 -1
- data/lib/io_streams/encode/reader.rb +1 -1
- data/lib/io_streams/encode/writer.rb +1 -1
- data/lib/io_streams/errors.rb +10 -0
- data/lib/io_streams/io_streams.rb +1 -1
- data/lib/io_streams/paths/file.rb +4 -4
- data/lib/io_streams/paths/http.rb +6 -3
- data/lib/io_streams/paths/s3.rb +1 -1
- data/lib/io_streams/paths/sftp.rb +16 -4
- data/lib/io_streams/pgp.rb +80 -61
- data/lib/io_streams/stream.rb +19 -8
- data/lib/io_streams/tabular.rb +5 -2
- data/lib/io_streams/tabular/header.rb +4 -1
- data/lib/io_streams/tabular/parser/fixed.rb +130 -25
- data/lib/io_streams/utils.rb +4 -4
- data/lib/io_streams/version.rb +1 -1
- data/lib/io_streams/zip/reader.rb +1 -1
- data/test/io_streams_test.rb +2 -2
- data/test/paths/file_test.rb +1 -1
- data/test/paths/s3_test.rb +2 -2
- data/test/paths/sftp_test.rb +4 -4
- data/test/pgp_test.rb +54 -4
- data/test/pgp_writer_test.rb +3 -3
- data/test/tabular_test.rb +55 -26
- data/test/test_helper.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2ff0af97e5f820b516ec7a7e695d1c4cb0f7ee7022d9463e6fe917e3c56554cd
|
4
|
+
data.tar.gz: 8f2831eb25377f9944280726e585efb524d0ab711da04aa6cb3a5872477e9635
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b5fe32aec626686316168b9bf627f01a86a7d0ab7aea6c56d4b522084a41ee7ac079ef9c6fb7fee20011db78ab9828adf39de75476dc33f5406b1d43b4066eb9
|
7
|
+
data.tar.gz: a59ded1990c6a7ffc13f42a65dc8f9dd89e1e42e19933c4b0aff026bdbf2712acc08c027293b44002ac9e937bd80916ffe4e4765b3903fcb85b286031de39acc
|
data/README.md
CHANGED
@@ -1,7 +1,8 @@
|
|
1
|
-
#
|
1
|
+
# IOStreams
|
2
2
|
[![Gem Version](https://img.shields.io/gem/v/iostreams.svg)](https://rubygems.org/gems/iostreams) [![Build Status](https://travis-ci.org/rocketjob/iostreams.svg?branch=master)](https://travis-ci.org/rocketjob/iostreams) [![Downloads](https://img.shields.io/gem/dt/iostreams.svg)](https://rubygems.org/gems/iostreams) [![License](https://img.shields.io/badge/license-Apache%202.0-brightgreen.svg)](http://opensource.org/licenses/Apache-2.0) ![](https://img.shields.io/badge/status-Production%20Ready-blue.svg) [![Gitter chat](https://img.shields.io/badge/IRC%20(gitter)-Support-brightgreen.svg)](https://gitter.im/rocketjob/support)
|
3
3
|
|
4
|
-
|
4
|
+
IOStreams is an incredibly powerful streaming library that makes changes to file formats, compression, encryption,
|
5
|
+
or storage mechanism transparent to the application.
|
5
6
|
|
6
7
|
## Project Status
|
7
8
|
|
@@ -9,7 +10,9 @@ Production Ready, heavily used in production environments, many as part of Rocke
|
|
9
10
|
|
10
11
|
## Documentation
|
11
12
|
|
12
|
-
[
|
13
|
+
Start with the [IOStreams tutorial](https://iostreams.rocketjob.io/tutorial) to get a great introduction to IOStreams.
|
14
|
+
|
15
|
+
Next, checkout the remaining [IOStreams documentation](https://iostreams.rocketjob.io/)
|
13
16
|
|
14
17
|
## Versioning
|
15
18
|
|
data/lib/io_streams/builder.rb
CHANGED
@@ -20,7 +20,7 @@ module IOStreams
|
|
20
20
|
raise(ArgumentError, "Cannot call #option unless the `file_name` was already set}") unless file_name
|
21
21
|
|
22
22
|
@options ||= {}
|
23
|
-
if opts = @options[stream]
|
23
|
+
if (opts = @options[stream])
|
24
24
|
opts.merge!(options)
|
25
25
|
else
|
26
26
|
@options[stream] = options.dup
|
@@ -40,7 +40,7 @@ module IOStreams
|
|
40
40
|
raise(ArgumentError, "Invalid stream: #{stream.inspect}") unless IOStreams.extensions.include?(stream)
|
41
41
|
|
42
42
|
@streams ||= {}
|
43
|
-
if opts = @streams[stream]
|
43
|
+
if (opts = @streams[stream])
|
44
44
|
opts.merge!(options)
|
45
45
|
else
|
46
46
|
@streams[stream] = options.dup
|
@@ -91,7 +91,8 @@ module IOStreams
|
|
91
91
|
private
|
92
92
|
|
93
93
|
def class_for_stream(type, stream)
|
94
|
-
ext = IOStreams.extensions[stream.nil? ? nil : stream.to_sym] ||
|
94
|
+
ext = IOStreams.extensions[stream.nil? ? nil : stream.to_sym] ||
|
95
|
+
raise(ArgumentError, "Unknown Stream type: #{stream.inspect}")
|
95
96
|
ext.send("#{type}_class") || raise(ArgumentError, "No #{type} registered for Stream type: #{stream.inspect}")
|
96
97
|
end
|
97
98
|
|
@@ -99,7 +100,7 @@ module IOStreams
|
|
99
100
|
def parse_extensions
|
100
101
|
parts = ::File.basename(file_name).split(".")
|
101
102
|
extensions = []
|
102
|
-
while extension = parts.pop
|
103
|
+
while (extension = parts.pop)
|
103
104
|
sym = extension.downcase.to_sym
|
104
105
|
break unless IOStreams.extensions[sym]
|
105
106
|
|
@@ -116,10 +117,12 @@ module IOStreams
|
|
116
117
|
block.call(io_stream)
|
117
118
|
elsif pipeline.size == 1
|
118
119
|
stream, opts = pipeline.first
|
119
|
-
class_for_stream(type, stream).open(io_stream, opts, &block)
|
120
|
+
class_for_stream(type, stream).open(io_stream, **opts, &block)
|
120
121
|
else
|
121
122
|
# Daisy chain multiple streams together
|
122
|
-
last = pipeline.keys.inject(block)
|
123
|
+
last = pipeline.keys.inject(block) do |inner, stream_sym|
|
124
|
+
->(io) { class_for_stream(type, stream_sym).open(io, **pipeline[stream_sym], &inner) }
|
125
|
+
end
|
123
126
|
last.call(io_stream)
|
124
127
|
end
|
125
128
|
end
|
@@ -206,7 +206,7 @@ module IOStreams
|
|
206
206
|
elsif streams.is_a?(Array)
|
207
207
|
streams.each { |stream| apply_old_style_streams(path, stream) }
|
208
208
|
elsif streams.is_a?(Hash)
|
209
|
-
streams.each_pair { |stream, options| path.stream(stream, options) }
|
209
|
+
streams.each_pair { |stream, options| path.stream(stream, **options) }
|
210
210
|
else
|
211
211
|
raise ArgumentError, "Invalid old style stream supplied: #{params.inspect}"
|
212
212
|
end
|
@@ -73,7 +73,7 @@ module IOStreams
|
|
73
73
|
# EOF reached?
|
74
74
|
return unless block
|
75
75
|
|
76
|
-
block = block.encode(@encoding,
|
76
|
+
block = block.encode(@encoding, **@encoding_options) unless block.encoding == @encoding
|
77
77
|
block = @cleaner.call(block, @replace) if @cleaner
|
78
78
|
block
|
79
79
|
end
|
@@ -66,7 +66,7 @@ module IOStreams
|
|
66
66
|
return 0 if data.nil?
|
67
67
|
|
68
68
|
data = data.to_s
|
69
|
-
block = data.encoding == @encoding ? data : data.encode(@encoding,
|
69
|
+
block = data.encoding == @encoding ? data : data.encode(@encoding, **@encoding_options)
|
70
70
|
block = @cleaner.call(block, @replace) if @cleaner
|
71
71
|
@output_stream.write(block)
|
72
72
|
end
|
data/lib/io_streams/errors.rb
CHANGED
@@ -18,5 +18,15 @@ module IOStreams
|
|
18
18
|
# When the specified delimiter is not found in the supplied stream / file
|
19
19
|
class DelimiterNotFound < Error
|
20
20
|
end
|
21
|
+
|
22
|
+
# Fixed length line has the wrong length
|
23
|
+
class InvalidLineLength < Error
|
24
|
+
end
|
25
|
+
|
26
|
+
class ValueTooLong < Error
|
27
|
+
end
|
28
|
+
|
29
|
+
class InvalidLayout < Error
|
30
|
+
end
|
21
31
|
end
|
22
32
|
end
|
@@ -58,7 +58,7 @@ module IOStreams
|
|
58
58
|
end
|
59
59
|
|
60
60
|
# For an existing IO Stream
|
61
|
-
# IOStreams.stream(io).file_name('blah.zip').encoding('BINARY').
|
61
|
+
# IOStreams.stream(io).file_name('blah.zip').encoding('BINARY').read
|
62
62
|
# IOStreams.stream(io).file_name('blah.zip').encoding('BINARY').each(:line){ ... }
|
63
63
|
# IOStreams.stream(io).file_name('blah.csv.zip').each(:line) { ... }
|
64
64
|
# IOStreams.stream(io).stream(:zip).stream(:pgp, passphrase: 'receiver_passphrase').read
|
@@ -15,16 +15,16 @@ module IOStreams
|
|
15
15
|
# Examples:
|
16
16
|
#
|
17
17
|
# # Case Insensitive file name lookup:
|
18
|
-
# IOStreams
|
18
|
+
# IOStreams.path("ruby").glob("r*.md") { |name| puts name }
|
19
19
|
#
|
20
20
|
# # Case Sensitive file name lookup:
|
21
|
-
# IOStreams
|
21
|
+
# IOStreams.path("ruby").each("R*.md", case_sensitive: true) { |name| puts name }
|
22
22
|
#
|
23
23
|
# # Also return the names of directories found during the search:
|
24
|
-
# IOStreams
|
24
|
+
# IOStreams.path("ruby").each("R*.md", directories: true) { |name| puts name }
|
25
25
|
#
|
26
26
|
# # Case Insensitive recursive file name lookup:
|
27
|
-
# IOStreams
|
27
|
+
# IOStreams.path("ruby").glob("**/*.md") { |name| puts name }
|
28
28
|
#
|
29
29
|
# Parameters:
|
30
30
|
# pattern [String]
|
@@ -26,16 +26,19 @@ module IOStreams
|
|
26
26
|
#
|
27
27
|
# http_redirect_count: [Integer]
|
28
28
|
# Maximum number of http redirects to follow.
|
29
|
-
def initialize(url, username: nil, password: nil, http_redirect_count: 10)
|
29
|
+
def initialize(url, username: nil, password: nil, http_redirect_count: 10, parameters: nil)
|
30
30
|
uri = URI.parse(url)
|
31
31
|
unless %w[http https].include?(uri.scheme)
|
32
|
-
raise(
|
32
|
+
raise(
|
33
|
+
ArgumentError,
|
34
|
+
"Invalid URL. Required Format: 'http://<host_name>/<file_name>', or 'https://<host_name>/<file_name>'"
|
35
|
+
)
|
33
36
|
end
|
34
37
|
|
35
38
|
@username = username || uri.user
|
36
39
|
@password = password || uri.password
|
37
40
|
@http_redirect_count = http_redirect_count
|
38
|
-
@url = url
|
41
|
+
@url = parameters ? "#{url}?#{URI.encode_www_form(parameters)}" : url
|
39
42
|
super(uri.path)
|
40
43
|
end
|
41
44
|
|
data/lib/io_streams/paths/s3.rb
CHANGED
@@ -92,7 +92,7 @@ module IOStreams
|
|
92
92
|
# encrypting data. This value is used to store the object and then it is
|
93
93
|
# discarded; Amazon does not store the encryption key. The key must be
|
94
94
|
# appropriate for use with the algorithm specified in the
|
95
|
-
# x-amz-server-side
|
95
|
+
# x-amz-server-side-encryption-customer-algorithm header.
|
96
96
|
#
|
97
97
|
# @option params [String] :sse_customer_key_md5
|
98
98
|
# Specifies the 128-bit MD5 digest of the encryption key according to
|
@@ -173,7 +173,10 @@ module IOStreams
|
|
173
173
|
writer.close
|
174
174
|
out = reader.read.chomp
|
175
175
|
unless waith_thr.value.success?
|
176
|
-
raise(
|
176
|
+
raise(
|
177
|
+
Errors::CommunicationsFailure,
|
178
|
+
"Download failed calling #{self.class.sftp_bin} via #{self.class.sshpass_bin}: #{out}"
|
179
|
+
)
|
177
180
|
end
|
178
181
|
|
179
182
|
out
|
@@ -183,7 +186,10 @@ module IOStreams
|
|
183
186
|
rescue StandardError
|
184
187
|
nil
|
185
188
|
end
|
186
|
-
raise(
|
189
|
+
raise(
|
190
|
+
Errors::CommunicationsFailure,
|
191
|
+
"Download failed calling #{self.class.sftp_bin} via #{self.class.sshpass_bin}: #{out}"
|
192
|
+
)
|
187
193
|
end
|
188
194
|
end
|
189
195
|
end
|
@@ -201,7 +207,10 @@ module IOStreams
|
|
201
207
|
writer.close
|
202
208
|
out = reader.read.chomp
|
203
209
|
unless waith_thr.value.success?
|
204
|
-
raise(
|
210
|
+
raise(
|
211
|
+
Errors::CommunicationsFailure,
|
212
|
+
"Upload failed calling #{self.class.sftp_bin} via #{self.class.sshpass_bin}: #{out}"
|
213
|
+
)
|
205
214
|
end
|
206
215
|
|
207
216
|
out
|
@@ -211,7 +220,10 @@ module IOStreams
|
|
211
220
|
rescue StandardError
|
212
221
|
nil
|
213
222
|
end
|
214
|
-
raise(
|
223
|
+
raise(
|
224
|
+
Errors::CommunicationsFailure,
|
225
|
+
"Upload failed calling #{self.class.sftp_bin} via #{self.class.sshpass_bin}: #{out}"
|
226
|
+
)
|
215
227
|
end
|
216
228
|
end
|
217
229
|
end
|
data/lib/io_streams/pgp.rb
CHANGED
@@ -46,7 +46,15 @@ module IOStreams
|
|
46
46
|
# `SecureRandom.urlsafe_base64(128)`
|
47
47
|
#
|
48
48
|
# See `man gpg` for the remaining options
|
49
|
-
def self.generate_key(name:,
|
49
|
+
def self.generate_key(name:,
|
50
|
+
email:,
|
51
|
+
comment: nil,
|
52
|
+
passphrase:,
|
53
|
+
key_type: "RSA",
|
54
|
+
key_length: 4096,
|
55
|
+
subkey_type: "RSA",
|
56
|
+
subkey_length: key_length,
|
57
|
+
expire_date: nil)
|
50
58
|
version_check
|
51
59
|
params = ""
|
52
60
|
params << "Key-Type: #{key_type}\n" if key_type
|
@@ -63,12 +71,11 @@ module IOStreams
|
|
63
71
|
|
64
72
|
out, err, status = Open3.capture3(command, binmode: true, stdin_data: params)
|
65
73
|
logger&.debug { "IOStreams::Pgp.generate_key: #{command}\n#{params}\n#{err}#{out}" }
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
raise(Pgp::Failure, "GPG Failed to generate key: #{err}#{out}")
|
74
|
+
|
75
|
+
raise(Pgp::Failure, "GPG Failed to generate key: #{err}#{out}") unless status.success?
|
76
|
+
|
77
|
+
if (match = err.match(/gpg: key ([0-9A-F]+)\s+/))
|
78
|
+
match[1]
|
72
79
|
end
|
73
80
|
end
|
74
81
|
|
@@ -77,7 +84,8 @@ module IOStreams
|
|
77
84
|
# Returns false if no key was found.
|
78
85
|
# Raises an exception if it fails to delete the key.
|
79
86
|
#
|
80
|
-
# email: [String]
|
87
|
+
# email: [String] Optional email address for the key.
|
88
|
+
# key_id: [String] Optional id for the key.
|
81
89
|
#
|
82
90
|
# public: [true|false]
|
83
91
|
# Whether to delete the public key
|
@@ -86,12 +94,12 @@ module IOStreams
|
|
86
94
|
# private: [true|false]
|
87
95
|
# Whether to delete the private key
|
88
96
|
# Default: false
|
89
|
-
def self.delete_keys(email
|
97
|
+
def self.delete_keys(email: nil, key_id: nil, public: true, private: false)
|
90
98
|
version_check
|
91
99
|
method_name = pgp_version.to_f >= 2.2 ? :delete_public_or_private_keys : :delete_public_or_private_keys_v1
|
92
100
|
status = false
|
93
|
-
status = send(method_name, email: email, private: true) if private
|
94
|
-
status = send(method_name, email: email, private: false) if public
|
101
|
+
status = send(method_name, email: email, key_id: key_id, private: true) if private
|
102
|
+
status = send(method_name, email: email, key_id: key_id, private: false) if public
|
95
103
|
status
|
96
104
|
end
|
97
105
|
|
@@ -150,16 +158,15 @@ module IOStreams
|
|
150
158
|
|
151
159
|
out, err, status = Open3.capture3(command, binmode: true, stdin_data: key)
|
152
160
|
logger&.debug { "IOStreams::Pgp.key_info: #{command}\n#{err}#{out}" }
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
end
|
161
|
+
|
162
|
+
raise(Pgp::Failure, "GPG Failed extracting key details: #{err} #{out}") unless status.success? && out.length.positive?
|
163
|
+
|
164
|
+
# Sample Output:
|
165
|
+
#
|
166
|
+
# pub 4096R/3A5456F5 2017-06-07
|
167
|
+
# uid Joe Bloggs <j@bloggs.net>
|
168
|
+
# sub 4096R/2C9B240B 2017-06-07
|
169
|
+
parse_list_output(out)
|
163
170
|
end
|
164
171
|
|
165
172
|
# Returns [String] containing all the public keys for the supplied email address.
|
@@ -181,11 +188,10 @@ module IOStreams
|
|
181
188
|
|
182
189
|
out, err, status = Open3.capture3(command, binmode: true)
|
183
190
|
logger&.debug { "IOStreams::Pgp.export: #{command}\n#{err}" }
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
end
|
191
|
+
|
192
|
+
raise(Pgp::Failure, "GPG Failed reading key: #{email}: #{err}") unless status.success? && out.length.positive?
|
193
|
+
|
194
|
+
out
|
189
195
|
end
|
190
196
|
|
191
197
|
# Imports the supplied public/private key
|
@@ -251,11 +257,14 @@ module IOStreams
|
|
251
257
|
def self.import_and_trust(key:)
|
252
258
|
raise(ArgumentError, "Key cannot be empty") if key.nil? || (key == "")
|
253
259
|
|
254
|
-
|
255
|
-
|
260
|
+
key_info = key_info(key: key).last
|
261
|
+
|
262
|
+
email = key_info.fetch(:email, nil)
|
263
|
+
key_id = key_info.fetch(:key_id, nil)
|
264
|
+
raise(ArgumentError, "Recipient email or key id cannot be extracted from supplied key") unless email || key_id
|
256
265
|
|
257
266
|
import(key: key)
|
258
|
-
set_trust(email: email)
|
267
|
+
set_trust(email: email, key_id: key_id)
|
259
268
|
email
|
260
269
|
end
|
261
270
|
|
@@ -266,20 +275,19 @@ module IOStreams
|
|
266
275
|
#
|
267
276
|
# After importing keys, they are not trusted and the relevant trust level must be set.
|
268
277
|
# Default: 5 : Ultimate
|
269
|
-
def self.set_trust(email
|
278
|
+
def self.set_trust(email: nil, key_id: nil, level: 5)
|
270
279
|
version_check
|
271
|
-
fingerprint = fingerprint(email: email)
|
280
|
+
fingerprint = key_id || fingerprint(email: email)
|
272
281
|
return unless fingerprint
|
273
282
|
|
274
283
|
command = "#{executable} --import-ownertrust"
|
275
284
|
trust = "#{fingerprint}:#{level + 1}:\n"
|
276
285
|
out, err, status = Open3.capture3(command, stdin_data: trust)
|
277
286
|
logger&.debug { "IOStreams::Pgp.set_trust: #{command}\n#{err}#{out}" }
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
end
|
287
|
+
|
288
|
+
raise(Pgp::Failure, "GPG Failed trusting key: #{err} #{out}") unless status.success?
|
289
|
+
|
290
|
+
err
|
283
291
|
end
|
284
292
|
|
285
293
|
# DEPRECATED - Use key_ids instead of fingerprints
|
@@ -287,18 +295,18 @@ module IOStreams
|
|
287
295
|
version_check
|
288
296
|
Open3.popen2e("#{executable} --list-keys --fingerprint --with-colons #{email}") do |_stdin, out, waith_thr|
|
289
297
|
output = out.read.chomp
|
290
|
-
|
291
|
-
output
|
292
|
-
|
293
|
-
return match[1]
|
294
|
-
end
|
298
|
+
unless waith_thr.value.success?
|
299
|
+
unless output =~ /(public key not found|No public key)/i
|
300
|
+
raise(Pgp::Failure, "GPG Failed calling #{executable} to list keys for #{email}: #{output}")
|
295
301
|
end
|
296
|
-
|
297
|
-
else
|
298
|
-
return if output =~ /(public key not found|No public key)/i
|
302
|
+
end
|
299
303
|
|
300
|
-
|
304
|
+
output.each_line do |line|
|
305
|
+
if (match = line.match(/\Afpr.*::([^\:]*):\Z/))
|
306
|
+
return match[1]
|
307
|
+
end
|
301
308
|
end
|
309
|
+
nil
|
302
310
|
end
|
303
311
|
end
|
304
312
|
|
@@ -328,7 +336,7 @@ module IOStreams
|
|
328
336
|
# CAMELLIA128, CAMELLIA192, CAMELLIA256
|
329
337
|
# Hash: MD5, SHA1, RIPEMD160, SHA256, SHA384, SHA512, SHA224
|
330
338
|
# Compression: Uncompressed, ZIP, ZLIB, BZIP2
|
331
|
-
if match = out.lines.first.match(/(\d+\.\d+.\d+)/)
|
339
|
+
if (match = out.lines.first.match(/(\d+\.\d+.\d+)/))
|
332
340
|
match[1]
|
333
341
|
end
|
334
342
|
else
|
@@ -348,9 +356,12 @@ module IOStreams
|
|
348
356
|
end
|
349
357
|
|
350
358
|
def self.version_check
|
351
|
-
|
352
|
-
|
353
|
-
|
359
|
+
return unless pgp_version.to_f >= 2.3
|
360
|
+
|
361
|
+
raise(
|
362
|
+
Pgp::UnsupportedVersion,
|
363
|
+
"Version #{pgp_version} of #{executable} is not yet supported. Please submit a Pull Request to support it."
|
364
|
+
)
|
354
365
|
end
|
355
366
|
|
356
367
|
# v2.2.1 output:
|
@@ -370,7 +381,7 @@ module IOStreams
|
|
370
381
|
results = []
|
371
382
|
hash = {}
|
372
383
|
out.each_line do |line|
|
373
|
-
if match = line.match(/(pub|sec)\s+(\D+)(\d+)\s+(\d+-\d+-\d+)\s+(.*)/)
|
384
|
+
if (match = line.match(/(pub|sec)\s+(\D+)(\d+)\s+(\d+-\d+-\d+)\s+(.*)/))
|
374
385
|
# v2.2: pub rsa1024 2017-10-24 [SCEA]
|
375
386
|
hash = {
|
376
387
|
private: match[1] == "sec",
|
@@ -382,7 +393,7 @@ module IOStreams
|
|
382
393
|
match[4]
|
383
394
|
end)
|
384
395
|
}
|
385
|
-
elsif match = line.match(%r{(pub|sec)\s+(\d+)(.*)/(\w+)\s+(\d+-\d+-\d+)(\s+(.+)<(.+)>)?})
|
396
|
+
elsif (match = line.match(%r{(pub|sec)\s+(\d+)(.*)/(\w+)\s+(\d+-\d+-\d+)(\s+(.+)<(.+)>)?}))
|
386
397
|
# Matches: pub 2048R/C7F9D9CB 2016-10-26
|
387
398
|
# Or: pub 2048R/C7F9D9CB 2016-10-26 Receiver <receiver@example.org>
|
388
399
|
hash = {
|
@@ -403,7 +414,7 @@ module IOStreams
|
|
403
414
|
results << hash
|
404
415
|
hash = {}
|
405
416
|
end
|
406
|
-
elsif match = line.match(/uid\s+(\[(.+)\]\s+)?(.+)<(.+)>/)
|
417
|
+
elsif (match = line.match(/uid\s+(\[(.+)\]\s+)?(.+)<(.+)>/))
|
407
418
|
# Matches: uid [ unknown] Joe Bloggs <j@bloggs.net>
|
408
419
|
# Or: uid Joe Bloggs <j@bloggs.net>
|
409
420
|
# v2.2: uid [ultimate] Joe Bloggs <pgp_test@iostreams.net>
|
@@ -412,7 +423,15 @@ module IOStreams
|
|
412
423
|
hash[:trust] = match[2].to_s.strip if match[1]
|
413
424
|
results << hash
|
414
425
|
hash = {}
|
415
|
-
elsif match = line.match(/([
|
426
|
+
elsif (match = line.match(/uid\s+(\[(.+)\]\s+)?(.+)/))
|
427
|
+
# Matches: uid [ unknown] Joe Bloggs
|
428
|
+
# Or: uid Joe Bloggs
|
429
|
+
# v2.2: uid [ultimate] Joe Bloggs
|
430
|
+
hash[:name] = match[3].to_s.strip
|
431
|
+
hash[:trust] = match[2].to_s.strip if match[1]
|
432
|
+
results << hash
|
433
|
+
hash = {}
|
434
|
+
elsif (match = line.match(/([A-Z0-9]+)/))
|
416
435
|
# v2.2 18A0FC1C09C0D8AE34CE659257DC4AE323C7368C
|
417
436
|
hash[:key_id] ||= match[1]
|
418
437
|
end
|
@@ -420,10 +439,10 @@ module IOStreams
|
|
420
439
|
results
|
421
440
|
end
|
422
441
|
|
423
|
-
def self.delete_public_or_private_keys(email
|
442
|
+
def self.delete_public_or_private_keys(email: nil, key_id: nil, private: false)
|
424
443
|
keys = private ? "secret-keys" : "keys"
|
425
444
|
|
426
|
-
list = list_keys(email: email, private: private)
|
445
|
+
list = email ? list_keys(email: email, private: private) : list_keys(key_id: key_id)
|
427
446
|
return false if list.empty?
|
428
447
|
|
429
448
|
list.each do |key_info|
|
@@ -435,17 +454,17 @@ module IOStreams
|
|
435
454
|
logger&.debug { "IOStreams::Pgp.delete_keys: #{command}\n#{err}#{out}" }
|
436
455
|
|
437
456
|
unless status.success?
|
438
|
-
raise(Pgp::Failure, "GPG Failed calling #{executable} to delete #{keys} for #{email}: #{err}: #{out}")
|
457
|
+
raise(Pgp::Failure, "GPG Failed calling #{executable} to delete #{keys} for #{email || key_id}: #{err}: #{out}")
|
439
458
|
end
|
440
|
-
raise(Pgp::Failure, "GPG Failed to delete #{keys} for #{email} #{err.strip}:#{out}") if out.include?("error")
|
459
|
+
raise(Pgp::Failure, "GPG Failed to delete #{keys} for #{email || key_id} #{err.strip}:#{out}") if out.include?("error")
|
441
460
|
end
|
442
461
|
true
|
443
462
|
end
|
444
463
|
|
445
|
-
def self.delete_public_or_private_keys_v1(email
|
464
|
+
def self.delete_public_or_private_keys_v1(email: nil, key_id: nil, private: false)
|
446
465
|
keys = private ? "secret-keys" : "keys"
|
447
466
|
|
448
|
-
command = "for i in `#{executable} --list-#{keys} --with-colons --fingerprint #{email} | grep \"^fpr\" | cut -d: -f10`; do\n"
|
467
|
+
command = "for i in `#{executable} --list-#{keys} --with-colons --fingerprint #{email || key_id} | grep \"^fpr\" | cut -d: -f10`; do\n"
|
449
468
|
command << "#{executable} --batch --no-tty --yes --delete-#{keys} \"$i\" ;\n"
|
450
469
|
command << "done"
|
451
470
|
|
@@ -454,9 +473,9 @@ module IOStreams
|
|
454
473
|
|
455
474
|
return false if err =~ /(not found|no public key)/i
|
456
475
|
unless status.success?
|
457
|
-
raise(Pgp::Failure, "GPG Failed calling #{executable} to delete #{keys} for #{email}: #{err}: #{out}")
|
476
|
+
raise(Pgp::Failure, "GPG Failed calling #{executable} to delete #{keys} for #{email || key_id}: #{err}: #{out}")
|
458
477
|
end
|
459
|
-
raise(Pgp::Failure, "GPG Failed to delete #{keys} for #{email} #{err.strip}: #{out}") if out.include?("error")
|
478
|
+
raise(Pgp::Failure, "GPG Failed to delete #{keys} for #{email || key_id} #{err.strip}: #{out}") if out.include?("error")
|
460
479
|
|
461
480
|
true
|
462
481
|
end
|
data/lib/io_streams/stream.rb
CHANGED
@@ -17,7 +17,7 @@ module IOStreams
|
|
17
17
|
#
|
18
18
|
# Example:
|
19
19
|
#
|
20
|
-
# IOStreams.path(
|
20
|
+
# IOStreams.path("tempfile2527").stream(:zip).stream(:pgp, passphrase: "receiver_passphrase").read
|
21
21
|
def stream(stream, **options)
|
22
22
|
builder.stream(stream, **options)
|
23
23
|
self
|
@@ -27,12 +27,12 @@ module IOStreams
|
|
27
27
|
# If the relevant stream is not found for this file it is ignored.
|
28
28
|
# For example, if the file does not have a pgp extension then the pgp option is not relevant.
|
29
29
|
#
|
30
|
-
# IOStreams.path(
|
30
|
+
# IOStreams.path("keep_safe.pgp").option(:pgp, passphrase: "receiver_passphrase").read
|
31
31
|
#
|
32
32
|
# # In this case the file is not pgp so the `passphrase` option is ignored.
|
33
|
-
# IOStreams.path(
|
33
|
+
# IOStreams.path("keep_safe.enc").option(:pgp, passphrase: "receiver_passphrase").read
|
34
34
|
#
|
35
|
-
# IOStreams.path(output_file_name).option(:pgp, passphrase:
|
35
|
+
# IOStreams.path(output_file_name).option(:pgp, passphrase: "receiver_passphrase").read
|
36
36
|
def option(stream, **options)
|
37
37
|
builder.option(stream, **options)
|
38
38
|
self
|
@@ -282,7 +282,12 @@ module IOStreams
|
|
282
282
|
def line_reader(embedded_within: nil, **args)
|
283
283
|
embedded_within = '"' if embedded_within.nil? && builder.file_name&.include?(".csv")
|
284
284
|
|
285
|
-
stream_reader
|
285
|
+
stream_reader do |io|
|
286
|
+
yield IOStreams::Line::Reader.new(io,
|
287
|
+
original_file_name: builder.file_name,
|
288
|
+
embedded_within: embedded_within,
|
289
|
+
**args)
|
290
|
+
end
|
286
291
|
end
|
287
292
|
|
288
293
|
# Iterate over a file / stream returning each line as an array, one at a time.
|
@@ -306,19 +311,25 @@ module IOStreams
|
|
306
311
|
def line_writer(**args, &block)
|
307
312
|
return block.call(io_stream) if io_stream&.is_a?(IOStreams::Line::Writer)
|
308
313
|
|
309
|
-
writer
|
314
|
+
writer do |io|
|
315
|
+
IOStreams::Line::Writer.stream(io, original_file_name: builder.file_name, **args, &block)
|
316
|
+
end
|
310
317
|
end
|
311
318
|
|
312
319
|
def row_writer(delimiter: $/, **args, &block)
|
313
320
|
return block.call(io_stream) if io_stream&.is_a?(IOStreams::Row::Writer)
|
314
321
|
|
315
|
-
line_writer(delimiter: delimiter)
|
322
|
+
line_writer(delimiter: delimiter) do |io|
|
323
|
+
IOStreams::Row::Writer.stream(io, original_file_name: builder.file_name, **args, &block)
|
324
|
+
end
|
316
325
|
end
|
317
326
|
|
318
327
|
def record_writer(delimiter: $/, **args, &block)
|
319
328
|
return block.call(io_stream) if io_stream&.is_a?(IOStreams::Record::Writer)
|
320
329
|
|
321
|
-
line_writer(delimiter: delimiter)
|
330
|
+
line_writer(delimiter: delimiter) do |io|
|
331
|
+
IOStreams::Record::Writer.stream(io, original_file_name: builder.file_name, **args, &block)
|
332
|
+
end
|
322
333
|
end
|
323
334
|
end
|
324
335
|
end
|
data/lib/io_streams/tabular.rb
CHANGED
@@ -89,7 +89,7 @@ module IOStreams
|
|
89
89
|
else
|
90
90
|
self.class.parser_class(format)
|
91
91
|
end
|
92
|
-
@parser = format_options ? klass.new(format_options) : klass.new
|
92
|
+
@parser = format_options ? klass.new(**format_options) : klass.new
|
93
93
|
end
|
94
94
|
|
95
95
|
# Returns [true|false] whether a header is still required in order to parse or render the current format.
|
@@ -142,7 +142,10 @@ module IOStreams
|
|
142
142
|
return unless requires_header?
|
143
143
|
|
144
144
|
if IOStreams::Utils.blank?(header.columns)
|
145
|
-
raise(
|
145
|
+
raise(
|
146
|
+
Errors::MissingHeader,
|
147
|
+
"Header columns must be set before attempting to render a header for format: #{format.inspect}"
|
148
|
+
)
|
146
149
|
end
|
147
150
|
|
148
151
|
parser.render(header.columns, header)
|
@@ -109,7 +109,10 @@ module IOStreams
|
|
109
109
|
end
|
110
110
|
|
111
111
|
unless row.is_a?(Array)
|
112
|
-
raise(
|
112
|
+
raise(
|
113
|
+
IOStreams::Errors::TypeMismatch,
|
114
|
+
"Don't know how to convert #{row.class.name} to an Array without the header columns being set."
|
115
|
+
)
|
113
116
|
end
|
114
117
|
|
115
118
|
row
|
@@ -3,31 +3,66 @@ module IOStreams
|
|
3
3
|
module Parser
|
4
4
|
# Parsing and rendering fixed length data
|
5
5
|
class Fixed < Base
|
6
|
-
attr_reader :
|
6
|
+
attr_reader :layout, :truncate
|
7
7
|
|
8
8
|
# Returns [IOStreams::Tabular::Parser]
|
9
9
|
#
|
10
10
|
# Parameters:
|
11
11
|
# layout: [Array<Hash>]
|
12
12
|
# [
|
13
|
-
# {
|
14
|
-
# {
|
15
|
-
# {
|
13
|
+
# {size: 23, key: "name"},
|
14
|
+
# {size: 40, key: "address"},
|
15
|
+
# {size: 2},
|
16
|
+
# {size: 5, key: "zip"},
|
17
|
+
# {size: 8, key: "age", type: :integer},
|
18
|
+
# {size: 10, key: "weight", type: :float, decimals: 2}
|
16
19
|
# ]
|
17
|
-
|
18
|
-
|
20
|
+
#
|
21
|
+
# Notes:
|
22
|
+
# * Leave out the name of the key to ignore that column during parsing,
|
23
|
+
# and to space fill when rendering. For example as a filler.
|
24
|
+
#
|
25
|
+
# Types:
|
26
|
+
# :string
|
27
|
+
# This is the default type.
|
28
|
+
# Applies space padding and the value is left justified.
|
29
|
+
# Returns value as a String
|
30
|
+
# :integer
|
31
|
+
# Applies zero padding to the left.
|
32
|
+
# Returns value as an Integer
|
33
|
+
# Raises Errors::ValueTooLong when the supplied value cannot be rendered in `size` characters.
|
34
|
+
# :float
|
35
|
+
# Applies zero padding to the left.
|
36
|
+
# Returns value as a float.
|
37
|
+
# The :size is the total size of this field including the `.` and the decimals.
|
38
|
+
# Number of :decimals
|
39
|
+
# Raises Errors::ValueTooLong when the supplied value cannot be rendered in `size` characters.
|
40
|
+
def initialize(layout:, truncate: true)
|
41
|
+
@layout = Layout.new(layout)
|
42
|
+
@truncate = truncate
|
43
|
+
end
|
44
|
+
|
45
|
+
# The required line length for every fixed length line
|
46
|
+
def line_length
|
47
|
+
layout.length
|
19
48
|
end
|
20
49
|
|
21
50
|
# Returns [String] fixed layout values extracted from the supplied hash.
|
22
|
-
#
|
51
|
+
#
|
52
|
+
# Notes:
|
53
|
+
# * A nil value is considered an empty string
|
54
|
+
# * When a supplied value exceeds the column size it is truncated.
|
23
55
|
def render(row, header)
|
24
56
|
hash = header.to_hash(row)
|
25
57
|
|
26
58
|
result = ""
|
27
|
-
|
28
|
-
|
29
|
-
value
|
30
|
-
|
59
|
+
layout.columns.each do |column|
|
60
|
+
value = hash[column.key].to_s
|
61
|
+
if !truncate && (value.length > column.size)
|
62
|
+
raise(Errors::ValueTooLong, "Value: #{value.inspect} is too long to fit into column #{column.key} of size #{column.size}")
|
63
|
+
end
|
64
|
+
|
65
|
+
result << column.render(value)
|
31
66
|
end
|
32
67
|
result
|
33
68
|
end
|
@@ -36,32 +71,102 @@ module IOStreams
|
|
36
71
|
# String will be encoded to `encoding`
|
37
72
|
def parse(line)
|
38
73
|
unless line.is_a?(String)
|
39
|
-
raise(
|
74
|
+
raise(Errors::TypeMismatch, "Line must be a String when format is :fixed. Actual: #{line.class.name}")
|
75
|
+
end
|
76
|
+
|
77
|
+
if line.length != layout.length
|
78
|
+
raise(Errors::InvalidLineLength, "Expected line length: #{layout.length}, actual line length: #{line.length}")
|
40
79
|
end
|
41
80
|
|
42
81
|
hash = {}
|
43
82
|
index = 0
|
44
|
-
|
45
|
-
|
46
|
-
index
|
47
|
-
|
83
|
+
layout.columns.each do |column|
|
84
|
+
# Ignore "columns" that have no keys. E.g. Fillers
|
85
|
+
hash[column.key] = column.parse(line[index, column.size]) if column.key
|
86
|
+
index += column.size
|
48
87
|
end
|
49
88
|
hash
|
50
89
|
end
|
51
90
|
|
52
91
|
private
|
53
92
|
|
54
|
-
|
93
|
+
class Layout
|
94
|
+
attr_reader :columns, :length
|
95
|
+
|
96
|
+
# Returns [Array<FixedLayout>] the layout for this fixed width file.
|
97
|
+
# Also validates values
|
98
|
+
def initialize(layout)
|
99
|
+
@length = 0
|
100
|
+
@columns = parse_layout(layout)
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
|
105
|
+
def parse_layout(layout)
|
106
|
+
@length = 0
|
107
|
+
layout.collect do |hash|
|
108
|
+
raise(Errors::InvalidLayout, "Missing required :size in: #{hash.inspect}") unless hash.key?(:size)
|
109
|
+
|
110
|
+
column = Column.new(**hash)
|
111
|
+
@length += column.size
|
112
|
+
column
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
class Column
|
118
|
+
TYPES = %i[string integer float].freeze
|
119
|
+
|
120
|
+
attr_reader :key, :size, :type, :decimals
|
121
|
+
|
122
|
+
def initialize(key: nil, size:, type: :string, decimals: 2)
|
123
|
+
@key = key
|
124
|
+
@size = size.to_i
|
125
|
+
@type = type.to_sym
|
126
|
+
@decimals = decimals
|
127
|
+
|
128
|
+
raise(Errors::InvalidLayout, "Size #{size.inspect} must be positive") unless @size.positive?
|
129
|
+
raise(Errors::InvalidLayout, "Unknown type: #{type.inspect}") unless TYPES.include?(type)
|
130
|
+
end
|
131
|
+
|
132
|
+
def parse(value)
|
133
|
+
return if value.nil?
|
134
|
+
|
135
|
+
stripped_value = value.to_s.strip
|
136
|
+
|
137
|
+
case type
|
138
|
+
when :string
|
139
|
+
stripped_value
|
140
|
+
when :integer
|
141
|
+
stripped_value.length.zero? ? nil : value.to_i
|
142
|
+
when :float
|
143
|
+
stripped_value.length.zero? ? nil : value.to_f
|
144
|
+
else
|
145
|
+
raise(Errors::InvalidLayout, "Unsupported type: #{type.inspect}")
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def render(value)
|
150
|
+
case type
|
151
|
+
when :string
|
152
|
+
format("%-#{size}.#{size}s", value.to_s)
|
153
|
+
when :integer
|
154
|
+
formatted = format("%0#{size}d", value.to_i)
|
155
|
+
if formatted.length > size
|
156
|
+
raise(Errors::ValueTooLong, "Value: #{value} is too large to fit into column:#{key} of size:#{size}")
|
157
|
+
end
|
55
158
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
raise(ArgumentError, "Missing required :key and :size in: #{map.inspect}") unless size && key
|
159
|
+
formatted
|
160
|
+
when :float
|
161
|
+
formatted = format("%0#{size}.#{decimals}f", value.to_f)
|
162
|
+
if formatted.length > size
|
163
|
+
raise(Errors::ValueTooLong, "Value: #{value} is too large to fit into column:#{key} of size:#{size}")
|
164
|
+
end
|
63
165
|
|
64
|
-
|
166
|
+
formatted
|
167
|
+
else
|
168
|
+
raise(Errors::InvalidLayout, "Unsupported type: #{type.inspect}")
|
169
|
+
end
|
65
170
|
end
|
66
171
|
end
|
67
172
|
end
|
data/lib/io_streams/utils.rb
CHANGED
@@ -49,10 +49,10 @@ module IOStreams
|
|
49
49
|
@user = uri.user
|
50
50
|
@password = uri.password
|
51
51
|
@port = uri.port
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
52
|
+
return unless uri.query
|
53
|
+
|
54
|
+
@query = {}
|
55
|
+
::URI.decode_www_form(uri.query).each { |key, value| @query[key] = value }
|
56
56
|
end
|
57
57
|
end
|
58
58
|
end
|
data/lib/io_streams/version.rb
CHANGED
data/test/io_streams_test.rb
CHANGED
@@ -16,7 +16,7 @@ module IOStreams
|
|
16
16
|
end
|
17
17
|
|
18
18
|
let :json_file_name do
|
19
|
-
"/tmp/
|
19
|
+
"/tmp/iostreams_abc.json"
|
20
20
|
end
|
21
21
|
|
22
22
|
describe ".root" do
|
@@ -90,7 +90,7 @@ module IOStreams
|
|
90
90
|
it "hash reader detects json format from file name" do
|
91
91
|
::File.open(json_file_name, "wb") { |file| file.write(expected_json) }
|
92
92
|
rows = []
|
93
|
-
path = IOStreams.path(
|
93
|
+
path = IOStreams.path(json_file_name)
|
94
94
|
path.each(:hash) do |row|
|
95
95
|
rows << row
|
96
96
|
end
|
data/test/paths/file_test.rb
CHANGED
data/test/paths/s3_test.rb
CHANGED
@@ -73,7 +73,7 @@ module Paths
|
|
73
73
|
|
74
74
|
describe "#reader" do
|
75
75
|
it "reads" do
|
76
|
-
assert_equal raw, existing_path.
|
76
|
+
assert_equal raw, existing_path.read
|
77
77
|
end
|
78
78
|
end
|
79
79
|
|
@@ -89,7 +89,7 @@ module Paths
|
|
89
89
|
|
90
90
|
describe "#writer" do
|
91
91
|
it "writes" do
|
92
|
-
assert_equal
|
92
|
+
assert_equal(raw.size, write_path.writer { |io| io.write(raw) })
|
93
93
|
assert write_path.exist?
|
94
94
|
assert_equal raw, write_path.read
|
95
95
|
end
|
data/test/paths/sftp_test.rb
CHANGED
@@ -42,7 +42,7 @@ module Paths
|
|
42
42
|
|
43
43
|
describe "#reader" do
|
44
44
|
it "reads" do
|
45
|
-
assert_equal raw, existing_path.
|
45
|
+
assert_equal raw, existing_path.read
|
46
46
|
end
|
47
47
|
|
48
48
|
it "fails when the file does not exist" do
|
@@ -60,7 +60,7 @@ module Paths
|
|
60
60
|
|
61
61
|
describe "#writer" do
|
62
62
|
it "writes" do
|
63
|
-
assert_equal
|
63
|
+
assert_equal(raw.size, write_path.writer { |io| io.write(raw) })
|
64
64
|
assert_equal raw, write_path.read
|
65
65
|
end
|
66
66
|
|
@@ -77,7 +77,7 @@ module Paths
|
|
77
77
|
|
78
78
|
it "writes" do
|
79
79
|
skip "No identity file env var set: SFTP_IDENTITY_FILE" unless ENV["SFTP_IDENTITY_FILE"]
|
80
|
-
assert_equal
|
80
|
+
assert_equal(raw.size, write_path.writer { |io| io.write(raw) })
|
81
81
|
assert_equal raw, write_path.read
|
82
82
|
end
|
83
83
|
end
|
@@ -90,7 +90,7 @@ module Paths
|
|
90
90
|
|
91
91
|
it "writes" do
|
92
92
|
skip "No identity file env var set: SFTP_IDENTITY_FILE" unless ENV["SFTP_IDENTITY_FILE"]
|
93
|
-
assert_equal
|
93
|
+
assert_equal(raw.size, write_path.writer { |io| io.write(raw) })
|
94
94
|
assert_equal raw, write_path.read
|
95
95
|
end
|
96
96
|
end
|
data/test/pgp_test.rb
CHANGED
@@ -70,7 +70,7 @@ class PgpTest < Minitest::Test
|
|
70
70
|
refute IOStreams::Pgp.delete_keys(email: "random@iostreams.net", public: true, private: true)
|
71
71
|
end
|
72
72
|
|
73
|
-
it "deletes existing keys" do
|
73
|
+
it "deletes existing keys with specified email" do
|
74
74
|
generated_key_id
|
75
75
|
# There is a timing issue with creating and then deleting keys.
|
76
76
|
# Call list_keys again to give GnuPGP time.
|
@@ -78,7 +78,16 @@ class PgpTest < Minitest::Test
|
|
78
78
|
assert IOStreams::Pgp.delete_keys(email: email, public: true, private: true)
|
79
79
|
end
|
80
80
|
|
81
|
-
it "deletes
|
81
|
+
it "deletes existing keys with specified key_id" do
|
82
|
+
generated_key_id
|
83
|
+
|
84
|
+
# There is a timing issue with creating and then deleting keys.
|
85
|
+
# Call list_keys again to give GnuPGP time.
|
86
|
+
IOStreams::Pgp.list_keys(key_id: generated_key_id, private: true)
|
87
|
+
assert IOStreams::Pgp.delete_keys(key_id: generated_key_id, public: true, private: true)
|
88
|
+
end
|
89
|
+
|
90
|
+
it "deletes just the private key with specified email" do
|
82
91
|
generated_key_id
|
83
92
|
# There is a timing issue with creating and then deleting keys.
|
84
93
|
# Call list_keys again to give GnuPGP time.
|
@@ -87,6 +96,16 @@ class PgpTest < Minitest::Test
|
|
87
96
|
refute IOStreams::Pgp.key?(key_id: generated_key_id, private: true)
|
88
97
|
assert IOStreams::Pgp.key?(key_id: generated_key_id, private: false)
|
89
98
|
end
|
99
|
+
|
100
|
+
it "deletes just the private key with specified key_id" do
|
101
|
+
generated_key_id
|
102
|
+
# There is a timing issue with creating and then deleting keys.
|
103
|
+
# Call list_keys again to give GnuPGP time.
|
104
|
+
IOStreams::Pgp.list_keys(key_id: generated_key_id, private: true)
|
105
|
+
assert IOStreams::Pgp.delete_keys(key_id: generated_key_id, public: false, private: true)
|
106
|
+
refute IOStreams::Pgp.key?(key_id: generated_key_id, private: true)
|
107
|
+
assert IOStreams::Pgp.key?(key_id: generated_key_id, private: false)
|
108
|
+
end
|
90
109
|
end
|
91
110
|
|
92
111
|
describe ".export" do
|
@@ -113,7 +132,7 @@ class PgpTest < Minitest::Test
|
|
113
132
|
IOStreams::Pgp.list_keys(email: email)
|
114
133
|
end
|
115
134
|
|
116
|
-
it "lists public keys" do
|
135
|
+
it "lists public keys for email" do
|
117
136
|
assert keys = IOStreams::Pgp.list_keys(email: email)
|
118
137
|
assert_equal 1, keys.size
|
119
138
|
assert key = keys.first
|
@@ -130,7 +149,24 @@ class PgpTest < Minitest::Test
|
|
130
149
|
assert_equal "ultimate", key[:trust] if (ver.to_f >= 2) && (maint >= 30)
|
131
150
|
end
|
132
151
|
|
133
|
-
it "lists
|
152
|
+
it "lists public keys for key_id" do
|
153
|
+
assert keys = IOStreams::Pgp.list_keys(key_id: generated_key_id)
|
154
|
+
assert_equal 1, keys.size
|
155
|
+
assert key = keys.first
|
156
|
+
|
157
|
+
assert_equal Date.today, key[:date]
|
158
|
+
assert_equal email, key[:email]
|
159
|
+
assert_includes key[:key_id], generated_key_id
|
160
|
+
assert_equal 1024, key[:key_length]
|
161
|
+
assert_includes %w[R rsa], key[:key_type]
|
162
|
+
assert_equal user_name, key[:name]
|
163
|
+
refute key[:private], key
|
164
|
+
ver = IOStreams::Pgp.pgp_version
|
165
|
+
maint = ver.split(".").last.to_i
|
166
|
+
assert_equal "ultimate", key[:trust] if (ver.to_f >= 2) && (maint >= 30)
|
167
|
+
end
|
168
|
+
|
169
|
+
it "lists private keys for email" do
|
134
170
|
assert keys = IOStreams::Pgp.list_keys(email: email, private: true)
|
135
171
|
assert_equal 1, keys.size
|
136
172
|
assert key = keys.first
|
@@ -143,6 +179,20 @@ class PgpTest < Minitest::Test
|
|
143
179
|
assert_equal user_name, key[:name]
|
144
180
|
assert key[:private], key
|
145
181
|
end
|
182
|
+
|
183
|
+
it "lists private keys for key_id" do
|
184
|
+
assert keys = IOStreams::Pgp.list_keys(key_id: generated_key_id, private: true)
|
185
|
+
assert_equal 1, keys.size
|
186
|
+
assert key = keys.first
|
187
|
+
|
188
|
+
assert_equal Date.today, key[:date]
|
189
|
+
assert_equal email, key[:email]
|
190
|
+
assert_includes key[:key_id], generated_key_id
|
191
|
+
assert_equal 1024, key[:key_length]
|
192
|
+
assert_includes %w[R rsa], key[:key_type]
|
193
|
+
assert_equal user_name, key[:name]
|
194
|
+
assert key[:private], key
|
195
|
+
end
|
146
196
|
end
|
147
197
|
|
148
198
|
describe ".key_info" do
|
data/test/pgp_writer_test.rb
CHANGED
@@ -53,9 +53,9 @@ class PgpWriterTest < Minitest::Test
|
|
53
53
|
end
|
54
54
|
|
55
55
|
it "supports multiple recipients" do
|
56
|
-
|
57
|
-
|
58
|
-
|
56
|
+
IOStreams::Pgp::Writer.file(file_name, recipient: %w[receiver@example.org receiver2@example.org], signer: "sender@example.org", signer_passphrase: "sender_passphrase") do |io|
|
57
|
+
io.write(decrypted)
|
58
|
+
end
|
59
59
|
|
60
60
|
result = IOStreams::Pgp::Reader.file(file_name, passphrase: "receiver_passphrase", &:read)
|
61
61
|
assert_equal decrypted, result
|
data/test/tabular_test.rb
CHANGED
@@ -138,33 +138,47 @@ class TabularTest < Minitest::Test
|
|
138
138
|
describe ":fixed format" do
|
139
139
|
let :tabular do
|
140
140
|
layout = [
|
141
|
-
{
|
142
|
-
{
|
143
|
-
{
|
141
|
+
{size: 23, key: :name},
|
142
|
+
{size: 40, key: :address},
|
143
|
+
{size: 2},
|
144
|
+
{size: 5, key: :zip, type: :integer},
|
145
|
+
{size: 8, key: :age, type: :integer},
|
146
|
+
{size: 10, key: :weight, type: :float, decimals: 2}
|
144
147
|
]
|
145
148
|
IOStreams::Tabular.new(format: :fixed, format_options: {layout: layout})
|
146
149
|
end
|
147
150
|
|
148
151
|
it "parses to hash" do
|
149
|
-
assert hash = tabular.record_parse("Jack over there
|
150
|
-
assert_equal({
|
152
|
+
assert hash = tabular.record_parse("Jack over there XX34618012345670012345.01")
|
153
|
+
assert_equal({name: "Jack", address: "over there", zip: 34_618, age: 1_234_567, weight: 12_345.01}, hash)
|
151
154
|
end
|
152
155
|
|
153
156
|
it "parses short string" do
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
+
assert_raises IOStreams::Errors::InvalidLineLength do
|
158
|
+
tabular.record_parse("Jack over th")
|
159
|
+
end
|
157
160
|
end
|
158
161
|
|
159
162
|
it "parses longer string" do
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
+
assert_raises IOStreams::Errors::InvalidLineLength do
|
164
|
+
tabular.record_parse("Jack over there XX34618012345670012345.01............")
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
it "parses zero values" do
|
169
|
+
assert hash = tabular.record_parse(" 00000000000000000000000")
|
170
|
+
assert_equal({name: "", address: "", zip: 0, age: 0, weight: 0.0}, hash)
|
171
|
+
end
|
172
|
+
|
173
|
+
it "parses empty values" do
|
174
|
+
assert hash = tabular.record_parse(" XX ")
|
175
|
+
assert_equal({name: "", address: "", zip: nil, age: nil, weight: nil}, hash)
|
163
176
|
end
|
164
177
|
|
165
|
-
it "parses
|
166
|
-
|
167
|
-
|
178
|
+
it "parses blank strings" do
|
179
|
+
skip "TODO: Part of tabular refactor to get this working"
|
180
|
+
assert hash = tabular.record_parse(" ")
|
181
|
+
assert_equal({name: "", address: "", zip: nil, age: nil, weight: nil}, hash)
|
168
182
|
end
|
169
183
|
|
170
184
|
it "parses nil data as nil" do
|
@@ -224,31 +238,46 @@ class TabularTest < Minitest::Test
|
|
224
238
|
describe ":fixed format" do
|
225
239
|
let :tabular do
|
226
240
|
layout = [
|
227
|
-
{
|
228
|
-
{
|
229
|
-
{
|
241
|
+
{size: 23, key: :name},
|
242
|
+
{size: 40, key: :address},
|
243
|
+
{size: 2},
|
244
|
+
{size: 5, key: :zip, type: :integer},
|
245
|
+
{size: 8, key: :age, type: :integer},
|
246
|
+
{size: 10, key: :weight, type: :float, decimals: 2}
|
230
247
|
]
|
231
248
|
IOStreams::Tabular.new(format: :fixed, format_options: {layout: layout})
|
232
249
|
end
|
233
250
|
|
234
251
|
it "renders fixed data" do
|
235
|
-
assert string = tabular.render(
|
236
|
-
assert_equal "Jack over there
|
252
|
+
assert string = tabular.render(name: "Jack", address: "over there", zip: 34_618, weight: 123_456.789123, age: 21)
|
253
|
+
assert_equal "Jack over there 34618000000210123456.79", string
|
237
254
|
end
|
238
255
|
|
239
|
-
it "truncates long
|
240
|
-
assert string = tabular.render(
|
241
|
-
assert_equal "Jack
|
256
|
+
it "truncates long strings" do
|
257
|
+
assert string = tabular.render(name: "Jack ran up the beanstalk and when jack reached the top it was truncated", address: "over there", zip: 34_618)
|
258
|
+
assert_equal "Jack ran up the beanstaover there 34618000000000000000.00", string
|
259
|
+
end
|
260
|
+
|
261
|
+
it "when integer is too large" do
|
262
|
+
assert_raises IOStreams::Errors::ValueTooLong do
|
263
|
+
tabular.render(zip: 3_461_832_653_653_265)
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
it "when float is too large" do
|
268
|
+
assert_raises IOStreams::Errors::ValueTooLong do
|
269
|
+
tabular.render(weight: 3_461_832_653_653_265.234)
|
270
|
+
end
|
242
271
|
end
|
243
272
|
|
244
273
|
it "renders nil as empty string" do
|
245
|
-
assert string = tabular.render(
|
246
|
-
assert_equal "
|
274
|
+
assert string = tabular.render(zip: 34_618)
|
275
|
+
assert_equal " 34618000000000000000.00", string
|
247
276
|
end
|
248
277
|
|
249
278
|
it "renders boolean" do
|
250
|
-
assert string = tabular.render(
|
251
|
-
assert_equal "true false
|
279
|
+
assert string = tabular.render(name: true, address: false)
|
280
|
+
assert_equal "true false 00000000000000000000.00", string
|
252
281
|
end
|
253
282
|
|
254
283
|
it "renders no data as nil" do
|
data/test/test_helper.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: iostreams
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Reid Morrison
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-07-13 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description:
|
14
14
|
email:
|
@@ -130,7 +130,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
130
130
|
- !ruby/object:Gem::Version
|
131
131
|
version: '0'
|
132
132
|
requirements: []
|
133
|
-
rubygems_version: 3.0.
|
133
|
+
rubygems_version: 3.0.8
|
134
134
|
signing_key:
|
135
135
|
specification_version: 4
|
136
136
|
summary: Input and Output streaming for Ruby.
|