iostreams 1.11.0 → 2.0.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.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +20 -2
  3. data/Rakefile +7 -0
  4. data/lib/io_streams/builder.rb +9 -9
  5. data/lib/io_streams/bzip2/writer.rb +1 -1
  6. data/lib/io_streams/encode/reader.rb +2 -2
  7. data/lib/io_streams/encode/writer.rb +5 -5
  8. data/lib/io_streams/gzip/reader.rb +1 -1
  9. data/lib/io_streams/gzip/writer.rb +1 -1
  10. data/lib/io_streams/io_streams.rb +45 -19
  11. data/lib/io_streams/line/reader.rb +2 -2
  12. data/lib/io_streams/line/writer.rb +1 -1
  13. data/lib/io_streams/path.rb +2 -2
  14. data/lib/io_streams/paths/file.rb +10 -10
  15. data/lib/io_streams/paths/http.rb +80 -7
  16. data/lib/io_streams/paths/matcher.rb +3 -3
  17. data/lib/io_streams/paths/s3.rb +3 -3
  18. data/lib/io_streams/paths/sftp.rb +7 -8
  19. data/lib/io_streams/pgp/reader.rb +23 -10
  20. data/lib/io_streams/pgp/writer.rb +93 -32
  21. data/lib/io_streams/pgp.rb +188 -60
  22. data/lib/io_streams/reader.rb +4 -4
  23. data/lib/io_streams/record/reader.rb +3 -4
  24. data/lib/io_streams/record/writer.rb +3 -4
  25. data/lib/io_streams/row/reader.rb +1 -1
  26. data/lib/io_streams/row/writer.rb +1 -1
  27. data/lib/io_streams/stream.rb +36 -30
  28. data/lib/io_streams/symmetric_encryption/reader.rb +2 -2
  29. data/lib/io_streams/symmetric_encryption/writer.rb +4 -4
  30. data/lib/io_streams/tabular/header.rb +18 -6
  31. data/lib/io_streams/tabular/parser/array.rb +0 -10
  32. data/lib/io_streams/tabular/parser/csv.rb +6 -38
  33. data/lib/io_streams/tabular/parser/fixed.rb +5 -5
  34. data/lib/io_streams/tabular/parser/psv.rb +0 -12
  35. data/lib/io_streams/tabular.rb +5 -10
  36. data/lib/io_streams/utils.rb +3 -2
  37. data/lib/io_streams/version.rb +1 -1
  38. data/lib/io_streams/writer.rb +6 -6
  39. data/lib/io_streams/xlsx/reader.rb +1 -1
  40. data/lib/io_streams/zip/writer.rb +22 -10
  41. data/lib/iostreams.rb +0 -1
  42. metadata +28 -111
  43. data/lib/io_streams/deprecated.rb +0 -216
  44. data/lib/io_streams/tabular/utility/csv_row.rb +0 -105
  45. data/test/builder_test.rb +0 -311
  46. data/test/bzip2_reader_test.rb +0 -27
  47. data/test/bzip2_writer_test.rb +0 -56
  48. data/test/deprecated_test.rb +0 -121
  49. data/test/encode_reader_test.rb +0 -51
  50. data/test/encode_writer_test.rb +0 -90
  51. data/test/files/embedded_lines_test.csv +0 -7
  52. data/test/files/multiple_files.zip +0 -0
  53. data/test/files/spreadsheet.xlsx +0 -0
  54. data/test/files/test.csv +0 -4
  55. data/test/files/test.json +0 -3
  56. data/test/files/test.psv +0 -4
  57. data/test/files/text file.txt +0 -3
  58. data/test/files/text.txt +0 -3
  59. data/test/files/text.txt.bz2 +0 -0
  60. data/test/files/text.txt.gz +0 -0
  61. data/test/files/text.txt.gz.zip +0 -0
  62. data/test/files/text.zip +0 -0
  63. data/test/files/text.zip.gz +0 -0
  64. data/test/files/unclosed_quote_large_test.csv +0 -1658
  65. data/test/files/unclosed_quote_test.csv +0 -4
  66. data/test/files/unclosed_quote_test2.csv +0 -3
  67. data/test/gzip_reader_test.rb +0 -27
  68. data/test/gzip_writer_test.rb +0 -52
  69. data/test/io_streams_test.rb +0 -132
  70. data/test/line_reader_test.rb +0 -325
  71. data/test/line_writer_test.rb +0 -59
  72. data/test/minimal_file_reader.rb +0 -25
  73. data/test/path_test.rb +0 -55
  74. data/test/paths/file_test.rb +0 -213
  75. data/test/paths/http_test.rb +0 -34
  76. data/test/paths/matcher_test.rb +0 -120
  77. data/test/paths/s3_test.rb +0 -220
  78. data/test/paths/sftp_test.rb +0 -106
  79. data/test/pgp_reader_test.rb +0 -46
  80. data/test/pgp_test.rb +0 -267
  81. data/test/pgp_writer_test.rb +0 -130
  82. data/test/record_reader_test.rb +0 -60
  83. data/test/record_writer_test.rb +0 -82
  84. data/test/row_reader_test.rb +0 -35
  85. data/test/row_writer_test.rb +0 -56
  86. data/test/stream_test.rb +0 -577
  87. data/test/tabular_test.rb +0 -338
  88. data/test/test_helper.rb +0 -40
  89. data/test/utils_test.rb +0 -20
  90. data/test/xlsx_reader_test.rb +0 -37
  91. data/test/zip_reader_test.rb +0 -53
  92. data/test/zip_writer_test.rb +0 -48
@@ -23,8 +23,6 @@ module IOStreams
23
23
  # output.write('Hello World')
24
24
  # end
25
25
  class SFTP < IOStreams::Path
26
- include SemanticLogger::Loggable if defined?(SemanticLogger)
27
-
28
26
  class << self
29
27
  attr_accessor :sshpass_bin, :sftp_bin, :sshpass_wait_seconds, :before_password_wait_seconds
30
28
  end
@@ -195,7 +193,7 @@ module IOStreams
195
193
  # Give time for password to be processed and stdin to be passed to sftp process.
196
194
  sleep self.class.sshpass_wait_seconds
197
195
 
198
- writer.puts "get #{remote_file_name} #{local_file_name}"
196
+ writer.puts "get #{remote_file_name.inspect} #{local_file_name.inspect}"
199
197
  writer.puts "bye"
200
198
  writer.close
201
199
  out = reader.read.chomp
@@ -311,7 +309,7 @@ module IOStreams
311
309
 
312
310
  def build_ssh_options
313
311
  options = ssh_options.dup
314
- options[:logger] ||= logger if defined?(SemanticLogger)
312
+ options[:logger] ||= IOStreams.logger if IOStreams.logger
315
313
  options[:port] ||= port
316
314
  options[:max_pkt_size] ||= 65_536
317
315
  options[:password] ||= @password
@@ -319,15 +317,16 @@ module IOStreams
319
317
  end
320
318
 
321
319
  def map_log_level
322
- return "INFO" unless defined?(SemanticLogger)
323
-
324
- case logger.level
320
+ level = IOStreams.logger&.level
321
+ case level
325
322
  when :trace
326
323
  "DEBUG3"
327
324
  when :warn
328
325
  "ERROR"
326
+ when Symbol
327
+ level.to_s
329
328
  else
330
- logger.level.to_s
329
+ "INFO"
331
330
  end
332
331
  end
333
332
  end
@@ -20,23 +20,36 @@ module IOStreams
20
20
  # Name of file to read from
21
21
  #
22
22
  # passphrase: [String]
23
- # Pass phrase for private key to decrypt the file with
24
- def self.file(file_name, passphrase: nil)
23
+ # Pass phrase for private key to decrypt the file with.
24
+ # Not required when the file is signed but not encrypted.
25
+ #
26
+ # ignore_mdc_error: [true|false]
27
+ # Decrypt files that lack MDC (Modification Detection Code) integrity protection.
28
+ # Some legacy/enterprise systems (e.g. Workday) still produce such files, which
29
+ # modern GnuPG refuses to decrypt with `gpg: decryption forced to fail!`.
30
+ # Only enable this for files from a trusted source: without MDC the decrypted
31
+ # contents are not protected against tampering.
32
+ # Default: false
33
+ def self.file(file_name, passphrase: nil, ignore_mdc_error: false)
25
34
  # Cannot use `passphrase: self.default_passphrase` since it is considered private
26
35
  passphrase ||= default_passphrase
27
- raise(ArgumentError, "Missing both passphrase and IOStreams::Pgp::Reader.default_passphrase") unless passphrase
28
36
 
37
+ args = []
29
38
  # Use --pinentry-mode loopback for all GnuPG versions >= 2.1
30
- loopback = IOStreams::Pgp.pgp_version.to_f >= 2.1 ? "--pinentry-mode loopback" : ""
31
-
39
+ args += ["--pinentry-mode", "loopback"] if IOStreams::Pgp.pgp_version.to_f >= 2.1
32
40
  # Use --no-symkey-cache for GnuPG versions >= 2.4 to avoid caching session keys
33
- no_symkey_cache = IOStreams::Pgp.pgp_version.to_f >= 2.4 ? "--no-symkey-cache" : ""
41
+ args << "--no-symkey-cache" if IOStreams::Pgp.pgp_version.to_f >= 2.4
42
+ args << "--ignore-mdc-error" if ignore_mdc_error
43
+ args += ["--batch", "--no-tty", "--yes", "--decrypt"]
44
+ # Only feed a passphrase when one is supplied; sign-only files need none.
45
+ args += ["--passphrase-fd", "0"] if passphrase
46
+ args << file_name.to_s
34
47
 
35
- command = "#{IOStreams::Pgp.executable} #{loopback} #{no_symkey_cache} --batch --no-tty --yes --decrypt --passphrase-fd 0 #{file_name}"
36
- IOStreams::Pgp.logger&.debug { "IOStreams::Pgp::Reader.open: #{command}" }
48
+ command = IOStreams::Pgp.gpg_command(*args)
49
+ IOStreams.logger&.debug { "IOStreams::Pgp::Reader.open: #{command.shelljoin}" }
37
50
 
38
51
  # Read decrypted contents from stdout
39
- Open3.popen3(command) do |stdin, stdout, stderr, waith_thr|
52
+ Open3.popen3(*command) do |stdin, stdout, stderr, waith_thr|
40
53
  stdin.puts(passphrase) if passphrase
41
54
  stdin.close
42
55
  result =
@@ -54,4 +67,4 @@ module IOStreams
54
67
  end
55
68
  end
56
69
  end
57
- end
70
+ end
@@ -26,18 +26,42 @@ module IOStreams
26
26
  @audit_recipient = nil
27
27
  end
28
28
 
29
- # Write to a PGP / GPG file, encrypting the contents as it is written.
29
+ # Write to a PGP / GPG file, encrypting and/or signing the contents as it is written.
30
30
  #
31
31
  # file_name: [String]
32
32
  # Name of file to write to.
33
33
  #
34
+ # encrypt: [true|false]
35
+ # Whether to encrypt the file for the supplied recipient(s).
36
+ # When set to false the file is signed but not encrypted, in which case a
37
+ # :signer must be supplied and :recipient / :import_and_trust_key are ignored.
38
+ # Default: true
39
+ #
34
40
  # recipient: [String|Array<String>]
35
41
  # One or more emails of users for which to encrypt the file.
42
+ # Ignored when encrypt is false.
36
43
  #
37
44
  # import_and_trust_key: [String|Array<String>]
38
45
  # One or more pgp keys to import and then use to encrypt the file.
39
46
  # Note: Ascii Keys can contain multiple keys, only the last one in the file is used.
40
47
  #
48
+ # import_and_trust_level: [Integer]
49
+ # The owner-trust level to assign to keys supplied via :import_and_trust_key.
50
+ # 1 : Undefined (no opinion)
51
+ # 2 : Never (do not trust)
52
+ # 3 : Marginal
53
+ # 4 : Full
54
+ # 5 : Ultimate
55
+ # Default: 5 : Ultimate
56
+ #
57
+ # SECURITY WARNING:
58
+ # Only import and trust keys received from a verified, trusted source.
59
+ # The default trust level is `5` (Ultimate), which tells GPG to treat the imported key
60
+ # as if it were one of your own keys. An ultimately trusted key is implicitly valid and
61
+ # can in turn confer validity on other keys it has signed. Importing an attacker supplied
62
+ # key at this level allows that attacker to impersonate other recipients.
63
+ # When the key cannot be fully verified, supply a lower `import_and_trust_level`.
64
+ #
41
65
  # signer: [String]
42
66
  # Name of user with which to sign the encypted file.
43
67
  # Default: default_signer or do not sign.
@@ -55,63 +79,100 @@ module IOStreams
55
79
  # compress_level: [Integer]
56
80
  # Compression level
57
81
  # Default: 6
82
+ #
83
+ # Note: There is intentionally no option here to disable MDC (Modification Detection
84
+ # Code) integrity protection on the files we produce. The reader exposes
85
+ # `ignore_mdc_error:` so we can *consume* legacy files that lack MDC (see Reader),
86
+ # but we never want to *generate* them: MDC is what protects the encrypted contents
87
+ # against tampering, and modern GnuPG mandates it for current ciphers anyway
88
+ # (`--disable-mdc` is a no-op unless an obsolete cipher is forced). Omitting MDC on
89
+ # output would only weaken files we create, with no upside for this library.
58
90
  def self.file(file_name,
91
+ encrypt: true,
59
92
  recipient: nil,
60
93
  import_and_trust_key: nil,
94
+ import_and_trust_level: 5,
61
95
  signer: default_signer,
62
96
  signer_passphrase: default_signer_passphrase,
63
97
  compress: :zip,
64
- compression: nil, # Deprecated
65
- compress_level: 6,
66
- original_file_name: nil)
67
-
68
- raise(ArgumentError, "Requires either :recipient or :import_and_trust_key") unless recipient || import_and_trust_key
69
-
70
- # Backward compatibility
71
- compress = compression if compression
98
+ compress_level: 6)
99
+ if encrypt
100
+ raise(ArgumentError, "Requires either :recipient or :import_and_trust_key") unless recipient || import_and_trust_key
101
+ elsif !signer
102
+ raise(ArgumentError, "Requires a :signer when encrypt is false")
103
+ end
72
104
 
73
105
  compress_level = 0 if compress == :none
74
106
 
75
- recipients = Array(recipient)
76
- recipients << audit_recipient if audit_recipient
77
-
78
- Array(import_and_trust_key).each do |key|
79
- recipients << IOStreams::Pgp.import_and_trust(key: key)
80
- end
107
+ recipients =
108
+ if encrypt
109
+ collect_recipients(recipient, import_and_trust_key, import_and_trust_level)
110
+ else
111
+ []
112
+ end
81
113
 
82
- # Write to stdin, with encrypted contents being written to the file
83
- command = "#{IOStreams::Pgp.executable} --batch --no-tty --yes --encrypt"
84
- command << " --sign --local-user \"#{signer}\"" if signer
85
- if signer_passphrase
86
- command << " --pinentry-mode loopback" if IOStreams::Pgp.pgp_version.to_f >= 2.1
87
- command << " --no-symkey-cache" if IOStreams::Pgp.pgp_version.to_f >= 2.4
88
- command << " --passphrase \"#{signer_passphrase}\""
89
- end
90
- command << " -z #{compress_level}" if compress_level != 6
91
- command << " --compress-algo #{compress}" unless compress == :none
92
- recipients.each { |address| command << " --recipient \"#{address}\"" }
93
- command << " -o \"#{file_name}\""
114
+ # Write to stdin, with the encrypted and/or signed contents being written to the file
115
+ args = build_args(
116
+ file_name: file_name,
117
+ encrypt: encrypt,
118
+ signer: signer,
119
+ signer_passphrase: signer_passphrase,
120
+ compress: compress,
121
+ compress_level: compress_level,
122
+ recipients: recipients
123
+ )
124
+ command = IOStreams::Pgp.gpg_command(*args)
94
125
 
95
- IOStreams::Pgp.logger&.debug { "IOStreams::Pgp::Writer.open: #{command}" }
126
+ # Do not log the command, it may contain the signer passphrase.
127
+ action = encrypt ? "encrypt" : "sign"
128
+ IOStreams.logger&.debug { "IOStreams::Pgp::Writer.open: #{action} -o #{file_name}" }
96
129
 
97
130
  result = nil
98
- Open3.popen2e(command) do |stdin, out, waith_thr|
131
+ Open3.popen2e(*command) do |stdin, out, waith_thr|
99
132
  begin
100
133
  stdin.binmode
101
134
  result = yield(stdin)
102
135
  stdin.close
103
136
  rescue Errno::EPIPE
104
137
  # Ignore broken pipe because gpg terminates early due to an error
105
- ::File.delete(file_name) if ::File.exist?(file_name)
138
+ ::FileUtils.rm_f(file_name)
106
139
  raise(Pgp::Failure, "GPG Failed writing to encrypted file: #{file_name}: #{out.read.chomp}")
107
140
  end
108
141
  unless waith_thr.value.success?
109
- ::File.delete(file_name) if ::File.exist?(file_name)
142
+ ::FileUtils.rm_f(file_name)
110
143
  raise(Pgp::Failure, "GPG Failed to create encrypted file: #{file_name}: #{out.read.chomp}")
111
144
  end
112
145
  end
113
146
  result
114
147
  end
148
+
149
+ def self.build_args(file_name:, encrypt:, signer:, signer_passphrase:, compress:, compress_level:, recipients:)
150
+ args = ["--batch", "--no-tty", "--yes"]
151
+ args << "--encrypt" if encrypt
152
+ args += ["--sign", "--local-user", signer.to_s] if signer
153
+ if signer_passphrase
154
+ args += ["--pinentry-mode", "loopback"] if IOStreams::Pgp.pgp_version.to_f >= 2.1
155
+ args << "--no-symkey-cache" if IOStreams::Pgp.pgp_version.to_f >= 2.4
156
+ args += ["--passphrase", signer_passphrase.to_s]
157
+ end
158
+ args += ["-z", compress_level.to_s] if compress_level != 6
159
+ args += ["--compress-algo", compress.to_s] unless compress == :none
160
+ recipients.each { |address| args += ["--recipient", address.to_s] }
161
+ args += ["-o", file_name.to_s]
162
+ args
163
+ end
164
+ private_class_method :build_args
165
+
166
+ def self.collect_recipients(recipient, import_and_trust_key, import_and_trust_level)
167
+ recipients = Array(recipient)
168
+ recipients << audit_recipient if audit_recipient
169
+
170
+ Array(import_and_trust_key).each do |key|
171
+ recipients << IOStreams::Pgp.import_and_trust(key: key, trust_level: import_and_trust_level)
172
+ end
173
+ recipients
174
+ end
175
+ private_class_method :collect_recipients
115
176
  end
116
177
  end
117
- end
178
+ end