rubyzip 2.4.1 → 3.7.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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/Changelog.md +549 -0
  3. data/LICENSE.md +24 -0
  4. data/README.md +250 -44
  5. data/Rakefile +15 -13
  6. data/lib/rubyzip.rb +3 -0
  7. data/lib/zip/central_directory.rb +194 -125
  8. data/lib/zip/compressor.rb +3 -1
  9. data/lib/zip/constants.rb +30 -21
  10. data/lib/zip/crypto/aes_encryption.rb +213 -0
  11. data/lib/zip/crypto/decrypted_io.rb +27 -15
  12. data/lib/zip/crypto/encryption.rb +13 -2
  13. data/lib/zip/crypto/null_encryption.rb +5 -13
  14. data/lib/zip/crypto/traditional_encryption.rb +13 -7
  15. data/lib/zip/decompressor.rb +4 -3
  16. data/lib/zip/deflater.rb +12 -8
  17. data/lib/zip/dirtyable.rb +32 -0
  18. data/lib/zip/dos_time.rb +64 -6
  19. data/lib/zip/entry.rb +432 -270
  20. data/lib/zip/entry_set.rb +11 -9
  21. data/lib/zip/errors.rb +152 -16
  22. data/lib/zip/extra_field/aes.rb +50 -0
  23. data/lib/zip/extra_field/generic.rb +10 -11
  24. data/lib/zip/extra_field/ntfs.rb +6 -4
  25. data/lib/zip/extra_field/old_unix.rb +5 -3
  26. data/lib/zip/extra_field/universal_time.rb +3 -1
  27. data/lib/zip/extra_field/unix.rb +7 -5
  28. data/lib/zip/extra_field/unknown.rb +35 -0
  29. data/lib/zip/extra_field/zip64.rb +19 -5
  30. data/lib/zip/extra_field.rb +25 -23
  31. data/lib/zip/file.rb +261 -262
  32. data/lib/zip/file_split.rb +91 -0
  33. data/lib/zip/filesystem/dir.rb +86 -0
  34. data/lib/zip/filesystem/directory_iterator.rb +48 -0
  35. data/lib/zip/filesystem/file.rb +263 -0
  36. data/lib/zip/filesystem/file_stat.rb +110 -0
  37. data/lib/zip/filesystem/zip_file_name_mapper.rb +81 -0
  38. data/lib/zip/filesystem.rb +27 -596
  39. data/lib/zip/inflater.rb +13 -10
  40. data/lib/zip/input_stream.rb +102 -61
  41. data/lib/zip/ioextras/abstract_input_stream.rb +156 -58
  42. data/lib/zip/ioextras/abstract_output_stream.rb +15 -3
  43. data/lib/zip/ioextras/fake_io.rb +39 -0
  44. data/lib/zip/ioextras.rb +12 -16
  45. data/lib/zip/null_compressor.rb +3 -1
  46. data/lib/zip/null_decompressor.rb +6 -3
  47. data/lib/zip/null_input_stream.rb +3 -1
  48. data/lib/zip/output_stream.rb +76 -64
  49. data/lib/zip/pass_thru_compressor.rb +6 -3
  50. data/lib/zip/pass_thru_decompressor.rb +12 -9
  51. data/lib/zip/streamable_directory.rb +3 -1
  52. data/lib/zip/streamable_stream.rb +4 -1
  53. data/lib/zip/version.rb +4 -1
  54. data/lib/zip.rb +28 -23
  55. data/rubyzip.gemspec +39 -0
  56. data/samples/example.rb +8 -3
  57. data/samples/example_filesystem.rb +3 -2
  58. data/samples/example_recursive.rb +3 -1
  59. data/samples/gtk_ruby_zip.rb +5 -3
  60. data/samples/qtzip.rb +7 -6
  61. data/samples/write_simple.rb +2 -1
  62. data/samples/zipfind.rb +1 -0
  63. metadata +88 -52
  64. data/TODO +0 -15
  65. data/lib/zip/extra_field/zip64_placeholder.rb +0 -15
@@ -1,3 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
1
4
  module Zip
2
5
  # InputStream is the basic class for reading zip entries in a
3
6
  # zip file. It is possible to create a InputStream object directly,
@@ -37,9 +40,8 @@ module Zip
37
40
  #
38
41
  # java.util.zip.ZipInputStream is the original inspiration for this
39
42
  # class.
40
-
41
43
  class InputStream
42
- CHUNK_SIZE = 32_768
44
+ CHUNK_SIZE = 32_768 # :nodoc:
43
45
 
44
46
  include ::Zip::IOExtras::AbstractInputStream
45
47
 
@@ -49,34 +51,37 @@ module Zip
49
51
  #
50
52
  # @param context [String||IO||StringIO] file path or IO/StringIO object
51
53
  # @param offset [Integer] offset in the IO/StringIO
52
- def initialize(context, dep_offset = 0, dep_decrypter = nil, offset: 0, decrypter: nil)
53
- super()
54
-
55
- if !dep_offset.zero? || !dep_decrypter.nil?
56
- Zip.warn_about_v3_api('Zip::InputStream.new')
57
- end
58
-
59
- offset = dep_offset if offset.zero?
60
- @archive_io = get_io(context, offset)
61
- @decompressor = ::Zip::NullDecompressor
62
- @decrypter = decrypter || dep_decrypter || ::Zip::NullDecrypter.new
54
+ # This method also accepts the standard IO encoding options:
55
+ # `external_encoding:`, `internal_encoding:` and `encoding:`.
56
+ def initialize(context, offset: 0, decrypter: nil, **opts)
57
+ super(**opts)
58
+ @archive_io = get_io(context, offset)
59
+ @decompressor = ::Zip::NullDecompressor
60
+ @decrypter = decrypter
63
61
  @current_entry = nil
62
+ @complete_entry = nil
64
63
  end
65
64
 
65
+ # Close this InputStream. All further IO will raise an IOError.
66
66
  def close
67
67
  @archive_io.close
68
68
  end
69
69
 
70
- # Returns a Entry object. It is necessary to call this
71
- # method on a newly created InputStream before reading from
72
- # the first entry in the archive. Returns nil when there are
73
- # no more entries.
70
+ # Returns an Entry object and positions the stream at the beginning of
71
+ # the entry data. It is necessary to call this method on a newly created
72
+ # InputStream before reading from the first entry in the archive.
73
+ # Returns nil when there are no more entries.
74
74
  def get_next_entry
75
- @archive_io.seek(@current_entry.next_header_offset, IO::SEEK_SET) if @current_entry
75
+ unless @current_entry.nil?
76
+ raise StreamingError, @current_entry if @current_entry.incomplete?
77
+
78
+ @archive_io.seek(@current_entry.next_header_offset, IO::SEEK_SET)
79
+ end
80
+
76
81
  open_entry
77
82
  end
78
83
 
79
- # Rewinds the stream to the beginning of the current entry
84
+ # Rewinds the stream to the beginning of the current entry.
80
85
  def rewind
81
86
  return if @current_entry.nil?
82
87
 
@@ -86,23 +91,44 @@ module Zip
86
91
  open_entry
87
92
  end
88
93
 
89
- # Modeled after IO.sysread
90
- def sysread(length = nil, outbuf = '')
91
- @decompressor.read(length, outbuf)
94
+ # Modelled after IO#sysread.
95
+ #
96
+ # Reads up to maxlen bytes from the stream; returns a string
97
+ # (either a new string or the given out_string).
98
+ # Its encoding is the unchanged encoding of out_string, if out_string is
99
+ # given; ASCII-8BIT, otherwise. Output contains maxlen bytes from the
100
+ # stream, if available; otherwise contains all available bytes, if any
101
+ # available; otherwise is an empty string.
102
+ #
103
+ # This method should not be used with buffered input stream-reader methods,
104
+ # such as #read, #readline, #gets.
105
+ def sysread(maxlen = nil, out_string = nil)
106
+ # Remove the default value for maxlen for version 4.
107
+ return (maxlen.nil? || maxlen.zero? ? '' : nil) if eof?
108
+
109
+ output = produce_input(maxlen)
110
+
111
+ if out_string.nil?
112
+ output.force_encoding(@internal_encoding || @external_encoding)
113
+ else
114
+ encoding = out_string.encoding
115
+ out_string.replace(output).force_encoding(encoding)
116
+ end
117
+ end
118
+
119
+ # Returns the size of the current entry, or `nil` if there isn't one.
120
+ def size
121
+ return if @current_entry.nil?
122
+
123
+ @current_entry.size
92
124
  end
93
125
 
94
126
  class << self
95
127
  # Same as #initialize but if a block is passed the opened
96
128
  # stream is passed to the block and closed when the block
97
129
  # returns.
98
- def open(filename_or_io, dep_offset = 0, dep_decrypter = nil, offset: 0, decrypter: nil)
99
- if !dep_offset.zero? || !dep_decrypter.nil?
100
- Zip.warn_about_v3_api('Zip::InputStream.new')
101
- end
102
-
103
- offset = dep_offset if offset.zero?
104
-
105
- zio = new(filename_or_io, offset: offset, decrypter: decrypter || dep_decrypter)
130
+ def open(filename_or_io, offset: 0, decrypter: nil, **opts)
131
+ zio = new(filename_or_io, offset: offset, decrypter: decrypter, **opts)
106
132
  return zio unless block_given?
107
133
 
108
134
  begin
@@ -111,17 +137,11 @@ module Zip
111
137
  zio.close if zio
112
138
  end
113
139
  end
114
-
115
- def open_buffer(filename_or_io, offset = 0)
116
- Zip.warn_about_v3_api('Zip::InputStream.open_buffer')
117
-
118
- ::Zip::InputStream.open(filename_or_io, offset)
119
- end
120
140
  end
121
141
 
122
142
  protected
123
143
 
124
- def get_io(io_or_file, offset = 0)
144
+ def get_io(io_or_file, offset = 0) # :nodoc:
125
145
  if io_or_file.respond_to?(:seek)
126
146
  io = io_or_file.dup
127
147
  io.seek(offset, ::IO::SEEK_SET)
@@ -133,57 +153,78 @@ module Zip
133
153
  end
134
154
  end
135
155
 
136
- def open_entry
156
+ def open_entry # :nodoc:
137
157
  @current_entry = ::Zip::Entry.read_local_entry(@archive_io)
138
- if @current_entry && @current_entry.encrypted? && @decrypter.kind_of?(NullEncrypter)
139
- raise Error, 'password required to decode zip file'
140
- end
158
+ return if @current_entry.nil?
141
159
 
142
- if @current_entry && @current_entry.incomplete? && @current_entry.crc == 0 \
143
- && @current_entry.compressed_size == 0 \
144
- && @current_entry.size == 0 && !@complete_entry
145
- raise GPFBit3Error,
146
- 'General purpose flag Bit 3 is set so not possible to get proper info from local header.' \
147
- 'Please use ::Zip::File instead of ::Zip::InputStream'
160
+ if @current_entry.incomplete? && @current_entry.compressed_size == 0 && !@complete_entry
161
+ raise StreamingError, @current_entry
148
162
  end
149
- @decrypted_io = get_decrypted_io
150
- @decompressor = get_decompressor
163
+
164
+ @decompressor = assemble_io
151
165
  flush
152
166
  @current_entry
153
167
  end
154
168
 
155
- def get_decrypted_io
169
+ def assemble_io # :nodoc:
170
+ io = if @current_entry.encrypted?
171
+ raise Error, 'A password is required to decode this zip file.' if @decrypter.nil?
172
+
173
+ get_decrypted_io
174
+ else
175
+ @archive_io
176
+ end
177
+
178
+ get_decompressor(io)
179
+ end
180
+
181
+ def get_decrypted_io # :nodoc:
156
182
  header = @archive_io.read(@decrypter.header_bytesize)
157
183
  @decrypter.reset!(header)
158
184
 
159
- ::Zip::DecryptedIo.new(@archive_io, @decrypter)
185
+ compressed_size =
186
+ if @current_entry.incomplete? && @current_entry.crc == 0 &&
187
+ @current_entry.compressed_size == 0 && @complete_entry
188
+ @complete_entry.compressed_size
189
+ else
190
+ @current_entry.compressed_size
191
+ end
192
+
193
+ if @decrypter.kind_of?(::Zip::AESDecrypter)
194
+ compressed_size -= @decrypter.header_bytesize
195
+ compressed_size -= ::Zip::AESEncryption::AUTHENTICATION_CODE_LENGTH
196
+ end
197
+
198
+ ::Zip::DecryptedIo.new(@archive_io, @decrypter, compressed_size)
160
199
  end
161
200
 
162
- def get_decompressor
201
+ def get_decompressor(io) # :nodoc:
163
202
  return ::Zip::NullDecompressor if @current_entry.nil?
164
203
 
165
204
  decompressed_size =
166
- if @current_entry.incomplete? && @current_entry.crc == 0 && @current_entry.size == 0 && @complete_entry
205
+ if @current_entry.incomplete? && @current_entry.crc == 0 &&
206
+ @current_entry.size == 0 && @complete_entry
167
207
  @complete_entry.size
168
208
  else
169
209
  @current_entry.size
170
210
  end
171
211
 
172
- decompressor_class = ::Zip::Decompressor.find_by_compression_method(@current_entry.compression_method)
212
+ decompressor_class = ::Zip::Decompressor.find_by_compression_method(
213
+ @current_entry.compression_method
214
+ )
173
215
  if decompressor_class.nil?
174
- raise ::Zip::CompressionMethodError,
175
- "Unsupported compression method #{@current_entry.compression_method}"
216
+ raise ::Zip::CompressionMethodError, @current_entry.compression_method
176
217
  end
177
218
 
178
- decompressor_class.new(@decrypted_io, decompressed_size)
219
+ decompressor_class.new(io, decompressed_size)
179
220
  end
180
221
 
181
- def produce_input
182
- @decompressor.read(CHUNK_SIZE)
222
+ def produce_input(maxlen = CHUNK_SIZE) # :nodoc:
223
+ @decompressor.read(maxlen)
183
224
  end
184
225
 
185
- def input_finished?
186
- @decompressor.eof
226
+ def input_finished? # :nodoc:
227
+ @decompressor.eof?
187
228
  end
188
229
  end
189
230
  end
@@ -1,122 +1,220 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'fake_io'
4
+
1
5
  module Zip
2
- module IOExtras
6
+ module IOExtras # :nodoc:
3
7
  # Implements many of the convenience methods of IO
4
- # such as gets, getc, readline and readlines
8
+ # such as gets, getc, read, readline and readlines
5
9
  # depends on: input_finished?, produce_input and read
6
10
  module AbstractInputStream
7
11
  include Enumerable
8
12
  include FakeIO
9
13
 
10
- def initialize
14
+ # Creates a new input stream wrapper.
15
+ #
16
+ # This method accepts the standard IO encoding options:
17
+ # `external_encoding:`, `internal_encoding:` and `encoding:`.
18
+ def initialize(**opts)
11
19
  super
12
20
  @lineno = 0
13
21
  @pos = 0
14
- @output_buffer = ''.b
22
+ @output_buffer = +''.b
15
23
  end
16
24
 
25
+ # Returns (or sets) the current line number in the decompressed
26
+ # (possibly decrypted) data stream. See the Line Number documentation
27
+ # for the IO class for more information.
17
28
  attr_accessor :lineno
29
+
30
+ # Returns the current position (in bytes) in the decompressed (possibly
31
+ # decrypted) data stream.
18
32
  attr_reader :pos
19
33
 
20
- def read(number_of_bytes = nil, buf = ''.b)
34
+ # Reads bytes from the stream decompressed (possibly decrypted) data
35
+ # stream. If `maxlen` is `nil`, reads all bytes; otherwise, reads up to
36
+ # `maxlen` bytes. If `maxlen` is zero, returns an empty string.
37
+ #
38
+ # Returns a string (either a new string or the given `out_string`)
39
+ # containing the bytes read. The string's encoding is the unchanged
40
+ # encoding of `out_string`, if `out_string` is given; `ASCII-8BIT`,
41
+ # otherwise.
42
+ def read(maxlen = nil, out_string = nil) # rubocop:disable Metrics/PerceivedComplexity,Metrics/CyclomaticComplexity
43
+ return (maxlen.nil? || maxlen.zero? ? '' : nil) if eof?
44
+
21
45
  tbuf = if @output_buffer.bytesize > 0
22
- if number_of_bytes <= @output_buffer.bytesize
23
- @output_buffer.slice!(0, number_of_bytes)
46
+ if maxlen && maxlen <= @output_buffer.bytesize
47
+ @output_buffer.slice!(0, maxlen)
24
48
  else
25
- number_of_bytes -= @output_buffer.bytesize if number_of_bytes
26
- rbuf = sysread(number_of_bytes, buf)
49
+ maxlen -= @output_buffer.bytesize if maxlen
50
+ rbuf = produce_input(maxlen)
27
51
  out = @output_buffer
28
52
  out << rbuf if rbuf
29
- @output_buffer = ''.b
53
+ @output_buffer = +''.b
30
54
  out
31
55
  end
32
56
  else
33
- sysread(number_of_bytes, buf)
57
+ produce_input(maxlen)
34
58
  end
35
59
 
36
60
  if tbuf.nil? || tbuf.empty?
37
- return nil if number_of_bytes
61
+ return nil if maxlen&.positive?
38
62
 
39
63
  return ''
40
64
  end
41
65
 
42
66
  @pos += tbuf.length
43
67
 
44
- if buf
45
- buf.replace(tbuf)
68
+ if out_string.nil?
69
+ tbuf.force_encoding(@internal_encoding || @external_encoding)
46
70
  else
47
- buf = tbuf
71
+ encoding = out_string.encoding
72
+ out_string.replace(tbuf).force_encoding(encoding)
48
73
  end
49
- buf
50
74
  end
51
75
 
52
- def readlines(a_sep_string = $INPUT_RECORD_SEPARATOR)
53
- ret_val = []
54
- each_line(a_sep_string) { |line| ret_val << line }
55
- ret_val
76
+ # Reads and returns all remaining lines from the stream. See the Line IO
77
+ # documentation in the IO class for more information.
78
+ #
79
+ # With no arguments given, returns lines as determined by line
80
+ # separator `$/`, or `nil` if none.
81
+ #
82
+ # With only string argument `sep` given, returns lines as
83
+ # determined by line separator `sep`, or `nil` if none. See the
84
+ # Line Separator documentation in the IO class for more information.
85
+ # The two special values for `sep` (`nil` and `""`) are honoured.
86
+ #
87
+ # With only integer argument `limit` given, limits the number of bytes
88
+ # in each line; see the Line Limit documentation in the IO class for more
89
+ # information.
90
+ #
91
+ # With arguments `sep` and `limit` given, combines the two behaviors.
92
+ #
93
+ # Optional keyword argument `chomp` specifies whether line separators
94
+ # are to be omitted.
95
+ def readlines(sep = $INPUT_RECORD_SEPARATOR, limit = nil, chomp: false)
96
+ each(sep, limit, chomp: chomp).to_a
56
97
  end
57
98
 
58
- def gets(a_sep_string = $INPUT_RECORD_SEPARATOR, number_of_bytes = nil)
59
- @lineno = @lineno.next
60
-
61
- if number_of_bytes.respond_to?(:to_int)
62
- number_of_bytes = number_of_bytes.to_int
63
- a_sep_string = a_sep_string.to_str if a_sep_string
64
- elsif a_sep_string.respond_to?(:to_int)
65
- number_of_bytes = a_sep_string.to_int
66
- a_sep_string = $INPUT_RECORD_SEPARATOR
67
- else
68
- number_of_bytes = nil
69
- a_sep_string = a_sep_string.to_str if a_sep_string
99
+ # Reads and returns a line from the stream. See the Line IO
100
+ # documentation in the IO class for more information.
101
+ #
102
+ # With no arguments given, returns the next line as determined by line
103
+ # separator `$/`, or `nil` if none.
104
+ #
105
+ # With only string argument `sep` given, returns the next line as
106
+ # determined by line separator `sep`, or `nil` if none. See the
107
+ # Line Separator documentation in the IO class for more information.
108
+ # The two special values for `sep` (`nil` and `""`) are honoured.
109
+ #
110
+ # With only integer argument `limit` given, limits the number of bytes
111
+ # in the line; see the Line Limit documentation in the IO class for more
112
+ # information.
113
+ #
114
+ # With arguments `sep` and `limit` given, combines the two behaviors.
115
+ #
116
+ # Optional keyword argument `chomp` specifies whether line separators
117
+ # are to be omitted.
118
+ def gets(sep = $INPUT_RECORD_SEPARATOR, limit = nil, chomp: false) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
119
+ if sep.nil?
120
+ return nil if eof?
121
+
122
+ @lineno = @lineno.next
123
+ return read(limit)
70
124
  end
71
125
 
72
- return read(number_of_bytes) if a_sep_string.nil?
126
+ if sep.respond_to?(:to_int)
127
+ limit = sep.to_int
128
+ sep = $INPUT_RECORD_SEPARATOR
129
+ elsif sep&.empty?
130
+ sep = "#{$INPUT_RECORD_SEPARATOR}#{$INPUT_RECORD_SEPARATOR}"
131
+ end
73
132
 
74
- a_sep_string = "#{$INPUT_RECORD_SEPARATOR}#{$INPUT_RECORD_SEPARATOR}" if a_sep_string.empty?
133
+ encoding = @internal_encoding || @external_encoding
75
134
 
76
135
  buffer_index = 0
77
- over_limit = (number_of_bytes && @output_buffer.bytesize >= number_of_bytes)
78
- while (match_index = @output_buffer.index(a_sep_string, buffer_index)).nil? && !over_limit
79
- buffer_index = [buffer_index, @output_buffer.bytesize - a_sep_string.bytesize].max
80
- return @output_buffer.empty? ? nil : flush if input_finished?
136
+ while (sep_index = @output_buffer.index(sep, buffer_index)).nil?
137
+ break if limit && @output_buffer.bytesize >= limit
138
+
139
+ if input_finished?
140
+ return nil if @output_buffer.empty?
141
+
142
+ @lineno = @lineno.next
143
+ @pos += @output_buffer.bytesize
144
+ return @output_buffer.slice!(0..).force_encoding(encoding)
145
+ end
81
146
 
147
+ buffer_index = [buffer_index, @output_buffer.bytesize - sep.bytesize].max
82
148
  @output_buffer << produce_input
83
- over_limit = (number_of_bytes && @output_buffer.bytesize >= number_of_bytes)
84
149
  end
85
- sep_index = [match_index + a_sep_string.bytesize, number_of_bytes || @output_buffer.bytesize].min
86
- @pos += sep_index
87
- @output_buffer.slice!(0...sep_index)
150
+
151
+ limit ||= @output_buffer.bytesize
152
+ cut_index = sep_index ? [sep_index + sep.bytesize, limit].min : limit
153
+ @lineno = @lineno.next
154
+ @pos += cut_index
155
+ data = @output_buffer.slice!(0, cut_index)
156
+ data&.chomp!(sep) if chomp
157
+ data&.force_encoding(encoding)
88
158
  end
89
159
 
90
- def ungetc(byte)
160
+ def ungetc(byte) # :nodoc:
91
161
  @output_buffer = byte.chr + @output_buffer
92
162
  end
93
163
 
94
- def flush
95
- ret_val = @output_buffer
96
- @output_buffer = ''.b
97
- ret_val
164
+ def flush # :nodoc:
165
+ @output_buffer.slice!(0..)
98
166
  end
99
167
 
100
- def readline(a_sep_string = $INPUT_RECORD_SEPARATOR)
101
- ret_val = gets(a_sep_string)
102
- raise EOFError unless ret_val
168
+ # Reads a line as with #gets, but raises `EOFError` if already at
169
+ # end-of-stream.
170
+ #
171
+ # Optional keyword argument `chomp` specifies whether line separators
172
+ # are to be omitted.
173
+ def readline(sep = $INPUT_RECORD_SEPARATOR, limit = nil, chomp: false)
174
+ raise EOFError if eof?
103
175
 
104
- ret_val
176
+ gets(sep, limit, chomp: chomp)
105
177
  end
106
178
 
107
- def each_line(a_sep_string = $INPUT_RECORD_SEPARATOR)
108
- loop { yield readline(a_sep_string) }
109
- rescue EOFError
110
- # We just need to catch this; we don't need to handle it.
179
+ # Calls the block with each remaining line read from the stream.
180
+ # Does nothing if already at end-of-stream. See the Line IO
181
+ # documentation in the IO class for more information.
182
+ #
183
+ # With no arguments given, reads lines as determined by line separator
184
+ # `$/`. With only string argument `sep` given, reads lines as determined
185
+ # by line separator `sep`. See the Line Separator documentation in the
186
+ # IO class for more information. The two special values for `sep`
187
+ # (`nil` and `""`) are honoured.
188
+ #
189
+ # With only integer argument `limit` given, limits the number of bytes
190
+ # in each line; see the Line Limit documentation in the IO class for
191
+ # more information.
192
+ #
193
+ # With arguments `sep` and `limit` given, combines the two behaviors.
194
+ #
195
+ # Optional keyword argument `chomp` specifies whether line separators
196
+ # are to be omitted.
197
+ #
198
+ # Returns an `Enumerator` if no block is given.
199
+ def each(sep = $INPUT_RECORD_SEPARATOR, limit = nil, chomp: false)
200
+ return to_enum(:each, sep, limit, chomp: chomp) unless block_given?
201
+
202
+ while (line = gets(sep, limit, chomp: chomp))
203
+ yield line
204
+ end
111
205
  end
112
206
 
113
- alias each each_line
207
+ alias each_line each
114
208
 
115
- def eof
209
+ # Returns `true` if the stream is positioned at its end, `false`
210
+ # otherwise. See Position documentation in the IO class for more
211
+ # information.
212
+ def eof?
116
213
  @output_buffer.empty? && input_finished?
117
214
  end
118
215
 
119
- alias eof? eof
216
+ # Alias for compatibility. Remove for version 4.
217
+ alias eof eof? # :nodoc:
120
218
  end
121
219
  end
122
220
  end
@@ -1,8 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'fake_io'
4
+
1
5
  module Zip
2
- module IOExtras
6
+ module IOExtras # :nodoc:
3
7
  # Implements many of the output convenience methods of IO.
4
8
  # relies on <<
5
- module AbstractOutputStream
9
+ module AbstractOutputStream # :nodoc:
6
10
  include FakeIO
7
11
 
8
12
  def write(data)
@@ -11,11 +15,19 @@ module Zip
11
15
  end
12
16
 
13
17
  def print(*params)
14
- self << params.join($OUTPUT_FIELD_SEPARATOR) << $OUTPUT_RECORD_SEPARATOR.to_s
18
+ self << params.join
19
+
20
+ # Deflate doesn't like `nil`s or empty strings!
21
+ unless $OUTPUT_RECORD_SEPARATOR.nil? || $OUTPUT_RECORD_SEPARATOR.empty?
22
+ self << $OUTPUT_RECORD_SEPARATOR
23
+ end
24
+
25
+ nil
15
26
  end
16
27
 
17
28
  def printf(a_format_string, *params)
18
29
  self << format(a_format_string, *params)
30
+ nil
19
31
  end
20
32
 
21
33
  def putc(an_object)
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zip
4
+ module IOExtras # :nodoc:
5
+ module FakeIO # :nodoc:
6
+ attr_reader :external_encoding, :internal_encoding
7
+
8
+ def initialize(**opts)
9
+ @internal_encoding = Encoding.find(opts[:internal_encoding]) if opts[:internal_encoding]
10
+
11
+ if opts[:encoding].kind_of?(String)
12
+ _ext, int = opts[:encoding].split(':', 2)
13
+ @internal_encoding = Encoding.find(int) unless @internal_encoding || int.nil?
14
+ end
15
+
16
+ @external_encoding = Encoding::ASCII_8BIT
17
+ end
18
+
19
+ def set_encoding(ext_enc, int_enc = nil)
20
+ if ext_enc.kind_of?(String) && ext_enc.include?(':')
21
+ _ext_enc, int_enc = ext_enc.split(':', 2)
22
+ end
23
+
24
+ begin
25
+ @internal_encoding = Encoding.find(int_enc) if int_enc
26
+ rescue ArgumentError
27
+ warn "warning: Unsupported encoding #{int_enc} ignored"
28
+ end
29
+
30
+ @external_encoding = Encoding::ASCII_8BIT
31
+ end
32
+
33
+ # Implement kind_of? in order to pretend to be an IO object.
34
+ def kind_of?(object)
35
+ object == IO || super
36
+ end
37
+ end
38
+ end
39
+ end
data/lib/zip/ioextras.rb CHANGED
@@ -1,35 +1,31 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Zip
2
- module IOExtras #:nodoc:
4
+ module IOExtras # :nodoc:
3
5
  CHUNK_SIZE = 131_072
4
6
 
5
- RANGE_ALL = 0..-1
6
-
7
7
  class << self
8
8
  def copy_stream(ostream, istream)
9
- ostream.write(istream.read(CHUNK_SIZE, ''.b)) until istream.eof?
9
+ ostream.write(istream.read(CHUNK_SIZE, +'')) until istream.eof?
10
10
  end
11
11
 
12
12
  def copy_stream_n(ostream, istream, nbytes)
13
13
  toread = nbytes
14
14
  while toread > 0 && !istream.eof?
15
- tr = toread > CHUNK_SIZE ? CHUNK_SIZE : toread
16
- ostream.write(istream.read(tr, ''.b))
17
- toread -= tr
18
- end
19
- end
20
- end
15
+ tr = [toread, CHUNK_SIZE].min
16
+ chunk = istream.read(tr, +'')
17
+ break if !chunk || chunk.empty?
21
18
 
22
- # Implements kind_of? in order to pretend to be an IO object
23
- module FakeIO
24
- def kind_of?(object)
25
- object == IO || super
19
+ ostream.write(chunk)
20
+ toread -= chunk.bytesize
21
+ end
26
22
  end
27
23
  end
28
24
  end
29
25
  end
30
26
 
31
- require 'zip/ioextras/abstract_input_stream'
32
- require 'zip/ioextras/abstract_output_stream'
27
+ require_relative 'ioextras/abstract_input_stream'
28
+ require_relative 'ioextras/abstract_output_stream'
33
29
 
34
30
  # Copyright (C) 2002-2004 Thomas Sondergaard
35
31
  # rubyzip is free software; you can redistribute it and/or
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Zip
2
- class NullCompressor < Compressor #:nodoc:all
4
+ class NullCompressor < Compressor # :nodoc:all
3
5
  include Singleton
4
6
 
5
7
  def <<(_data)