iostreams 1.2.1 → 1.3.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/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
|
[](https://rubygems.org/gems/iostreams) [](https://travis-ci.org/rocketjob/iostreams) [](https://rubygems.org/gems/iostreams) [](http://opensource.org/licenses/Apache-2.0)  [-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.
|