bzip2-ffi 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,10 +1,13 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
1
4
  require 'ffi'
2
5
 
3
6
  module Bzip2
4
7
  module FFI
5
8
  # FFI bindings for the libbz2 low-level interface.
6
9
  #
7
- # See bzlib.h and http://bzip.org/docs.html.
10
+ # See bzlib.h and https://sourceware.org/bzip2/docs.html.
8
11
  #
9
12
  # @private
10
13
  module Libbz2 #:nodoc:
@@ -50,7 +53,7 @@ module Bzip2
50
53
 
51
54
  :bzalloc, :bzalloc,
52
55
  :bzfree, :bzfree,
53
- :opaque, :pointer
56
+ :opaque, :pointer
54
57
  end
55
58
 
56
59
  # int BZ2_bzCompressInt(bz_stream* strm, int blockSize100k, int verbosity, int workFactor);
@@ -61,7 +64,7 @@ module Bzip2
61
64
 
62
65
  # int BZ2_bzCompressEnd (bz_stream* strm);
63
66
  attach_function :BZ2_bzCompressEnd, [BzStream.by_ref], :int
64
-
67
+
65
68
  # int BZ2_bzDecompressInit (bz_stream *strm, int verbosity, int small);
66
69
  attach_function :BZ2_bzDecompressInit, [BzStream.by_ref, :int, :int], :int
67
70
 
@@ -71,5 +74,6 @@ module Bzip2
71
74
  # int BZ2_bzDecompressEnd (bz_stream *strm);
72
75
  attach_function :BZ2_bzDecompressEnd, [BzStream.by_ref], :int
73
76
  end
77
+ private_constant :Libbz2
74
78
  end
75
79
  end
@@ -1,10 +1,13 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
1
4
  require 'pathname'
2
5
  require 'stringio'
3
6
 
4
7
  module Bzip2
5
8
  module FFI
6
- # `Reader` reads and decompresses a bzip2 compressed stream or file. The
7
- # public instance methods of `Reader` are intended to be equivalent to those
9
+ # {Reader} reads and decompresses a bzip2 compressed stream or file. The
10
+ # public instance methods of {Reader} are intended to be equivalent to those
8
11
  # of a standard `IO` object.
9
12
  #
10
13
  # Data can be read as a stream using {open} and {#read}, for example:
@@ -15,7 +18,7 @@ module Bzip2
15
18
  # end
16
19
  # end
17
20
  #
18
- # Alternatively, without passing a block to `open`:
21
+ # Alternatively, without passing a block to {open}:
19
22
  #
20
23
  # reader = Bzip2::FFI::Reader.open(io_or_path)
21
24
  # begin
@@ -26,77 +29,88 @@ module Bzip2
26
29
  # reader.close
27
30
  # end
28
31
  #
29
- # An entire bzip2 structure can be read in a single step using {read}:
32
+ # All the available bzipped data can be read in a single step using {read}:
30
33
  #
31
34
  # uncompressed = Bzip2::FFI::Reader.read(io_or_path)
32
35
  #
33
- # The {open} and {read} methods accept either an `IO`-like object or a file
34
- # path. `IO`-like objects must have a `read` method. Paths can be given as
36
+ # The {open} and {read} methods accept either an IO-like object or a file
37
+ # path. IO-like objects must have a `#read` method. Paths can be given as
35
38
  # either a `String` or `Pathname`.
36
39
  #
37
40
  # No character conversion is performed on decompressed bytes. The {read} and
38
41
  # {#read} methods return instances of `String` that represent the raw
39
- # decompressed bytes, with `encoding` set to `Encoding::ASCII_8BIT` (also
42
+ # decompressed bytes, with `#encoding` set to `Encoding::ASCII_8BIT` (also
40
43
  # known as `Encoding::BINARY`).
41
44
  #
42
- # `Reader` will read a single bzip2 compressed structure from the given
43
- # stream or file. If the stream or file contains data beyond the end of
44
- # the bzip2 structure, such data may be read during decompression. If such
45
- # an overread has occurred and the `IO`-like object being read from has a
46
- # `seek` method, `Reader` will use it to reposition the stream to the byte
47
- # immediately following the end of the bzip2 structure. If `seek` raises
48
- # an `IOError`, it will be caught and the stream position will be left
49
- # unchanged.
45
+ # {Reader} will normally read all consecutive bzip2 compressed structure
46
+ # from the given stream or file (unless the `:first_only` parameter is
47
+ # specified - see {open}). If the stream or file contains additional data
48
+ # beyond the end of the compressed bzip2 data, it may be read during
49
+ # decompression. If such an overread has occurred and the IO-like object
50
+ # being read from has a `#seek` method, {Reader} will use it to reposition
51
+ # the stream to the byte immediately following the end of the compressed
52
+ # bzip2 data. If `#seek` raises an `IOError`, it will be caught and the
53
+ # stream position will be left unchanged.
50
54
  class Reader < IO
51
55
  # The number of bytes read from the compressed data stream at a time.
52
56
  #
53
57
  # @private
54
58
  READ_BUFFER_SIZE = 4096 #:nodoc:
59
+ private_constant :READ_BUFFER_SIZE
55
60
 
56
61
  # The number of uncompressed bytes to read at a time when using {#read}
57
62
  # without a length.
58
63
  #
59
64
  # @private
60
65
  DEFAULT_DECOMPRESS_COUNT = 4096 #:nodoc:
66
+ private_constant :DEFAULT_DECOMPRESS_COUNT
61
67
 
62
68
  class << self
63
69
  # Use send to keep this hidden from YARD (visibility tag does not work).
64
70
  send(:public, :new)
65
71
 
66
- # Opens a {Reader} to read and decompress data from either an `IO`-like
67
- # object or a file. `IO`-like objects must have a `read` method. Files
72
+ # Opens a {Reader} to read and decompress data from either an IO-like
73
+ # object or a file. IO-like objects must have a `#read` method. Files
68
74
  # can be specified using either a `String` containing the file path or a
69
75
  # `Pathname`.
70
76
  #
71
- # If no block is given, the opened `Reader` instance is returned. After
77
+ # If no block is given, the opened {Reader} instance is returned. After
72
78
  # use, the instance should be closed using the {#close} method.
73
79
  #
74
- # If a block is given, it will be passed the opened `Reader` instance
75
- # as an argument. After the block terminates, the `Reader` instance will
76
- # automatically be closed. `open` will then return the result of the
80
+ # If a block is given, it will be passed the opened {Reader} instance
81
+ # as an argument. After the block terminates, the {Reader} instance will
82
+ # automatically be closed. {open} will then return the result of the
77
83
  # block.
78
84
  #
79
85
  # The following options can be specified using the `options` `Hash`:
80
86
  #
81
- # * `:autoclose` - When passing an `IO`-like object, set to `true` to
82
- # close the `IO` when the `Reader` instance is closed.
87
+ # * `:autoclose` - When passing an IO-like object, set to `true` to
88
+ # close it when the {Reader} instance is closed.
89
+ # * `:first_only` - Bzip2 files can contain multiple consecutive
90
+ # compressed strctures. Normally all the structures
91
+ # will be decompressed with the decompressed bytes
92
+ # concatenated. Set to `true` to only read the first
93
+ # structure.
83
94
  # * `:small` - Set to `true` to use an alternative decompression
84
95
  # algorithm that uses less memory, but at the cost of
85
96
  # decompressing more slowly (roughly 2,300 kB less memory
86
97
  # at about half the speed).
87
98
  #
88
- # If an `IO`-like object that has a `binmode` method is passed to
89
- # `open`, `binmode` will be called on `io_or_path` before yielding to
90
- # the block or returning.
99
+ # If an IO-like object that has a `#binmode` method is passed to {open},
100
+ # `#binmode` will be called on `io_or_path` before yielding to the block
101
+ # or returning.
91
102
  #
92
- # @param io_or_path [Object] Either an `IO`-like object with a `read`
103
+ # @param io_or_path [Object] Either an IO-like object with a `#read`
93
104
  # method or a file path as a `String` or
94
105
  # `Pathname`.
95
106
  # @param options [Hash] Optional parameters (`:autoclose` and `:small`).
96
- # @return [Object] The opened `Reader` instance if no block is given, or
107
+ # @yield [reader] If a block is given, it is yielded to.
108
+ # @yieldparam reader [Reader] The new {Reader} instance.
109
+ # @yieldresult [Object] A result to be returned as the result of {open}.
110
+ # @return [Object] The opened {Reader} instance if no block is given, or
97
111
  # the result of the block if a block is given.
98
112
  # @raise [ArgumentError] If `io_or_path` is _not_ a `String`, `Pathname`
99
- # or an `IO`-like object with a `read` method.
113
+ # or an IO-like object with a `#read` method.
100
114
  # @raise [Errno::ENOENT] If the specified file does not exist.
101
115
  # @raise [Error::Bzip2Error] If an error occurs when initializing
102
116
  # libbz2.
@@ -113,37 +127,42 @@ module Bzip2
113
127
  end
114
128
 
115
129
  # Reads and decompresses and entire bzip2 compressed structure from
116
- # either an `IO`-like object or a file and returns the decompressed
117
- # bytes as a `String`. `IO`-like objects must have a `read` method.
118
- # Files can be specified using either a `String` containing the file
119
- # path or a `Pathname`.
130
+ # either an IO-like object or a file and returns the decompressed bytes
131
+ # as a `String`. IO-like objects must have a `#read` method. Files can
132
+ # be specified using either a `String` containing the file path or a
133
+ # `Pathname`.
120
134
  #
121
135
  # The following options can be specified using the `options` `Hash`:
122
136
  #
123
- # * `:autoclose` - When passing an `IO`-like object, set to `true` to
124
- # close the `IO` when the compressed data has been
125
- # read.
137
+ # * `:autoclose` - When passing an IO-like object, set to `true` to
138
+ # close it when the compressed data has been read.
139
+ # * `:first_only` - Bzip2 files can contain multiple consecutive
140
+ # compressed strctures. Normally all the structures
141
+ # will be decompressed with the decompressed bytes
142
+ # concatenated. Set to `true` to only read the first
143
+ # structure.
126
144
  # * `:small` - Set to `true` to use an alternative decompression
127
145
  # algorithm that uses less memory, but at the cost of
128
146
  # decompressing more slowly (roughly 2,300 kB less memory
129
147
  # at about half the speed).
130
148
  #
131
- # No character conversion is performed on decompressed bytes. `read`
149
+ # No character conversion is performed on decompressed bytes. {read}
132
150
  # returns a `String` that represents the raw decompressed bytes, with
133
151
  # `encoding` set to `Encoding::ASCII_8BIT` (also known as
134
152
  # `Encoding::BINARY`).
135
153
  #
136
- # If an `IO`-like object that has a `binmode` method is passed to
137
- # `read`, `binmode` will be called on `io_or_path` before any compressed
138
- # data is read.
154
+ # If an IO-like object that has a `#inmode` method is passed to {read},
155
+ # `#binmode` will be called on `io_or_path` before any compressed data
156
+ # is read.
139
157
  #
140
- # @param io_or_path [Object] Either an `IO`-like object with a `read`
158
+ # @param io_or_path [Object] Either an IO-like object with a `#read`
141
159
  # method or a file path as a `String` or
142
160
  # `Pathname`.
143
- # @param options [Hash] Optional parameters (`:autoclose` and `:small`).
161
+ # @param options [Hash] Optional parameters (`:autoclose`, `:first_only`
162
+ # and `:small`).
144
163
  # @return [String] The decompressed data.
145
164
  # @raise [ArgumentError] If `io_or_path` is _not_ a `String`, `Pathname`
146
- # or an `IO`-like object with a `read` method.
165
+ # or an IO-like object with a `#read` method.
147
166
  # @raise [Errno::ENOENT] If the specified file does not exist.
148
167
  # @raise [Error::Bzip2Error] If an error occurs when initializing
149
168
  # libbz2 or decompressing data.
@@ -155,11 +174,11 @@ module Bzip2
155
174
 
156
175
  private
157
176
 
158
- # Returns a Proc that can be used as a finalizer to call
159
- # `BZ2_bzDecompressEnd` with the given `stream`.
177
+ # Returns a `Proc` that can be used as a finalizer to call
178
+ # {Libbz2::BZ2_bzDecompressEnd} with the given `stream`.
160
179
  #
161
180
  # @param stream [Libbz2::BzStream] The stream that should be passed to
162
- # `BZ2_bzDecompressEnd`.
181
+ # {Libbz2::BZ2_bzDecompressEnd}.
163
182
  def finalize(stream)
164
183
  ->(id) do
165
184
  Libbz2::BZ2_bzDecompressEnd(stream)
@@ -167,65 +186,74 @@ module Bzip2
167
186
  end
168
187
  end
169
188
 
170
- # Initializes a {Reader} to read compressed data from an `IO`-like object
171
- # (`io`). `io` must have a `read` method.
189
+ # Initializes a {Reader} to read compressed data from an IO-like object
190
+ # (`io`). `io` must have a `#read` method.
172
191
  #
173
192
  # The following options can be specified using the `options` `Hash`:
174
193
  #
175
- # * `:autoclose` - Set to `true` to close `io` when the `Reader` instance
194
+ # * `:autoclose` - Set to `true` to close `io` when the {Reader} instance
176
195
  # is closed.
196
+ # * `:first_only` - Bzip2 files can contain multiple consecutive
197
+ # compressed strctures. Normally all the structures will
198
+ # be decompressed with the decompressed bytes
199
+ # concatenated. Set to `true` to only read the first
200
+ # structure.
177
201
  # * `:small` - Set to `true` to use an alternative decompression
178
202
  # algorithm that uses less memory, but at the cost of
179
203
  # decompressing more slowly (roughly 2,300 kB less memory
180
204
  # at about half the speed).
181
205
  #
182
- # `binmode` is called on `io` if `io` responds to `binmode`.
206
+ # `#binmode` is called on `io` if `io` responds to `#binmode`.
183
207
  #
184
- # After use, the `Reader` instance should be closed using the {#close}
208
+ # After use, the {Reader} instance should be closed using the {#close}
185
209
  # method.
186
210
  #
187
- # @param io [Object] An `IO`-like object with a `read` method.
188
- # @param options [Hash] Optional parameters (`:autoclose` and `:small`).
189
- # @raise [ArgumentError] If `io` is `nil` or does not respond to `read`.
211
+ # @param io [Object] An IO-like object with a `#read` method.
212
+ # @param options [Hash] Optional parameters (`:autoclose`, `:first_only`
213
+ # and `:small`).
214
+ # @raise [ArgumentError] If `io` is `nil` or does not respond to `#read`.
190
215
  # @raise [Error::Bzip2Error] If an error occurs when initializing libbz2.
191
216
  def initialize(io, options = {})
192
217
  super
193
218
  raise ArgumentError, 'io must respond to read' unless io.respond_to?(:read)
194
219
 
195
- small = options[:small]
220
+ @first_only = options[:first_only]
221
+ @small = options[:small] ? 1 : 0
196
222
 
197
223
  @in_eof = false
198
224
  @out_eof = false
199
225
  @in_buffer = nil
226
+ @structure_number = 1
227
+ @structure_start_pos = 0
228
+ @in_pos = 0
229
+ @out_pos = 0
200
230
 
201
- check_error(Libbz2::BZ2_bzDecompressInit(stream, 0, small ? 1 : 0))
202
-
203
- ObjectSpace.define_finalizer(self, self.class.send(:finalize, stream))
231
+ decompress_init(stream)
204
232
  end
205
233
 
206
234
  # Ends decompression and closes the {Reader}.
207
235
  #
208
236
  # If the {open} method is used with a block, it is not necessary to call
209
- # `close`. Otherwise, `close` should be called once the `Reader` is no
237
+ # {#close}. Otherwise, {#close} should be called once the {Reader} is no
210
238
  # longer needed.
211
239
  #
212
240
  # @return [NilType] `nil`.
213
- # @raise [IOError] If the `Reader` has already been closed.
241
+ # @raise [IOError] If the {Reader} has already been closed.
214
242
  def close
215
243
  s = stream
216
244
 
217
245
  unless @out_eof
218
246
  decompress_end(s)
219
247
  end
220
-
248
+
221
249
  s[:next_in] = nil
222
250
  s[:next_out] = nil
223
-
251
+
224
252
  if @in_buffer
225
253
  @in_buffer.free
226
254
  @in_buffer = nil
227
255
  end
228
-
256
+
229
257
  super
230
258
  end
231
259
 
@@ -240,17 +268,17 @@ module Bzip2
240
268
  # A result of `nil` or a `String` with a length less than `length` bytes
241
269
  # indicates that the end of the decompressed data has been reached.
242
270
  #
243
- # If `length` is `nil`, `read` reads until the end of the decompressed
271
+ # If `length` is `nil`, {#read} reads until the end of the decompressed
244
272
  # data, returning the uncompressed bytes as a `String`.
245
273
  #
246
- # If `length` is 0, `read` returns an empty `String`.
274
+ # If `length` is 0, {#read} returns an empty `String`.
247
275
  #
248
276
  # If the optional `buffer` argument is present, it must reference a
249
277
  # `String` that will receive the decompressed data. `buffer` will
250
- # contain only the decompressed data after the call to `read`, even if it
278
+ # contain only the decompressed data after the call to {#read}, even if it
251
279
  # is not empty beforehand.
252
280
  #
253
- # No character conversion is performed on decompressed bytes. `read`
281
+ # No character conversion is performed on decompressed bytes. {#read}
254
282
  # returns a `String` that represents the raw decompressed bytes, with
255
283
  # `encoding` set to `Encoding::ASCII_8BIT` (also known as
256
284
  # `Encoding::BINARY`).
@@ -267,7 +295,7 @@ module Bzip2
267
295
  # the end of the decompressed data has been reached.
268
296
  # @raise [ArgumentError] If `length` is negative.
269
297
  # @raise [Error::Bzip2Error] If an error occurs during decompression.
270
- # @raise [IOError] If the `Reader` has been closed.
298
+ # @raise [IOError] If the {Reader} has been closed.
271
299
  def read(length = nil, buffer = nil)
272
300
  if buffer
273
301
  buffer.clear
@@ -279,11 +307,11 @@ module Bzip2
279
307
 
280
308
  if length == 0
281
309
  check_closed
282
- return buffer || ''
310
+ return buffer || String.new
283
311
  end
284
312
 
285
313
  decompressed = decompress(length)
286
-
314
+
287
315
  return nil unless decompressed
288
316
  buffer ? buffer << decompressed : decompressed
289
317
  else
@@ -292,11 +320,11 @@ module Bzip2
292
320
  # StringIO#binmode is a no-op, but call in case it is implemented in
293
321
  # future versions.
294
322
  result.binmode
295
-
323
+
296
324
  result.set_encoding(Encoding::ASCII_8BIT)
297
325
 
298
326
  loop do
299
- decompressed = decompress(DEFAULT_DECOMPRESS_COUNT)
327
+ decompressed = decompress(DEFAULT_DECOMPRESS_COUNT)
300
328
  break unless decompressed
301
329
  result.write(decompressed)
302
330
  break if decompressed.bytesize < DEFAULT_DECOMPRESS_COUNT
@@ -306,6 +334,29 @@ module Bzip2
306
334
  end
307
335
  end
308
336
 
337
+ # Returns `true` if decompression has completed, otherwise `false`.
338
+ #
339
+ # Note that it is possible for `false` to be returned after all the
340
+ # decompressed data has been read. In such cases, the next call to {#read}
341
+ # will detect the end of the bzip2 structure and set {#eof?} to `true`.
342
+ #
343
+ # @return [Boolean] If decompression has completed, otherwise `false`.
344
+ # @raise [IOError] If the {Reader} has been closed.
345
+ def eof?
346
+ check_closed
347
+ @out_eof
348
+ end
349
+ alias eof eof?
350
+
351
+ # Returns the number of decompressed bytes that have been read.
352
+ #
353
+ # @return [Integer] The number of decompressed bytes that have been read.
354
+ # @raise [IOError] If the {Reader} has been closed.
355
+ def pos
356
+ check_closed
357
+ @out_pos
358
+ end
359
+
309
360
  private
310
361
 
311
362
  # Attempts to decompress and return `count` bytes.
@@ -315,9 +366,9 @@ module Bzip2
315
366
  # @return [String] The decompressed data as a `String` with ASCII-8BIT
316
367
  # encoding, or `nil` if length was a positive integer and
317
368
  # the end of the decompressed data has been reached.
318
- # @raise [ArgumentError] if `count` is not greater than or equal to 1.
369
+ # @raise [ArgumentError] If `count` is not greater than or equal to 1.
319
370
  # @raise [Error::Bzip2Error] If an error occurs during decompression.
320
- # @raise [IOError] If the `Reader` has been closed.
371
+ # @raise [IOError] If the {Reader} has been closed.
321
372
  def decompress(count)
322
373
  raise ArgumentError, "count must be a positive integer" unless count >= 1
323
374
  s = stream
@@ -335,6 +386,7 @@ module Bzip2
335
386
  bytes = io.read(READ_BUFFER_SIZE)
336
387
 
337
388
  if bytes && bytes.bytesize > 0
389
+ @in_pos += bytes.bytesize
338
390
  @in_eof = bytes.bytesize < READ_BUFFER_SIZE
339
391
  @in_buffer = ::FFI::MemoryPointer.new(1, bytes.bytesize)
340
392
  @in_buffer.write_bytes(bytes)
@@ -345,8 +397,15 @@ module Bzip2
345
397
  end
346
398
  end
347
399
 
400
+ # Reached the end of input without reading anything in the current
401
+ # bzip2 structure. No more data to process.
402
+ if @in_pos == @structure_start_pos
403
+ @out_eof = true
404
+ break
405
+ end
406
+
348
407
  prev_avail_out = s[:avail_out]
349
-
408
+
350
409
  res = Libbz2::BZ2_bzDecompress(s)
351
410
 
352
411
  if s[:avail_in] == 0 && @in_buffer
@@ -355,43 +414,51 @@ module Bzip2
355
414
  @in_buffer = nil
356
415
  end
357
416
 
358
- check_error(res)
359
-
360
- if res == Libbz2::BZ_STREAM_END
361
- # The input could contain data after the end of the bzip2 stream.
362
- #
363
- # s[:avail_in] will contain the number of bytes that have been
364
- # read from io, but not been consumed by BZ2_bzDecompress.
417
+ if @structure_number > 1 && res == Libbz2::BZ_DATA_ERROR_MAGIC
418
+ # Found something other than the bzip2 magic bytes after the end
419
+ # of a bzip2 structure.
365
420
  #
366
421
  # Attempt to move the input stream back by the amount that has
367
422
  # been over-read.
368
- if s[:avail_in] > 0 && io.respond_to?(:seek)
369
- io.seek(-s[:avail_in], ::IO::SEEK_CUR) rescue IOError
370
- end
371
-
372
- if @in_buffer
373
- s[:next_in] = nil
374
- @in_buffer.free
375
- @in_buffer = nil
376
- end
377
-
423
+ attempt_seek_to_structure_start
378
424
  decompress_end(s)
379
-
380
425
  @out_eof = true
381
426
  break
382
427
  end
383
428
 
384
- break if s[:avail_out] == 0
429
+ check_error(res)
430
+
431
+ if res == Libbz2::BZ_STREAM_END
432
+ decompress_end(s)
385
433
 
386
- # No more input available and calling BZ2_bzDecompress didn't
387
- # advance the output. Raise an error.
388
- if @in_eof && prev_avail_out == s[:avail_out]
389
- raise Error::UnexpectedEofError.new
434
+ if (s[:avail_in] > 0 || !@in_eof) && !@first_only
435
+ # Re-initialize to read a second bzip2 structure if there is
436
+ # still input available and not restricting to the first stream.
437
+ @structure_number += 1
438
+ @structure_start_pos = @in_pos - s[:avail_in]
439
+ decompress_init(s)
440
+ else
441
+ # May have already read data after the end of the first bzip2
442
+ # structure.
443
+ attempt_seek_to_structure_start if @first_only
444
+ @out_eof = true
445
+ break
446
+ end
447
+ else
448
+ # No more input available and calling BZ2_bzDecompress didn't
449
+ # advance the output. Raise an error.
450
+ if @in_eof && s[:avail_in] == 0 && prev_avail_out == s[:avail_out]
451
+ decompress_end(s)
452
+ @out_eof = true
453
+ raise Error::UnexpectedEofError.new
454
+ end
390
455
  end
456
+
457
+ break if s[:avail_out] == 0
391
458
  end
392
459
 
393
460
  result = out_buffer.read_bytes(out_buffer.size - s[:avail_out])
394
- ensure
461
+ ensure
395
462
  out_buffer.free
396
463
  s[:next_out] = nil
397
464
  s[:avail_out] = 0
@@ -400,18 +467,51 @@ module Bzip2
400
467
  if @out_eof && result.bytesize == 0
401
468
  nil
402
469
  else
470
+ @out_pos += result.bytesize
403
471
  result
404
- end
472
+ end
473
+ end
474
+
475
+ # Attempts to reposition the compressed stream to the start of the current
476
+ # structure. Used when {Libbz2::BZ2_bzDecompress} has read beyond the end
477
+ # of a bzip2 structure.
478
+ def attempt_seek_to_structure_start
479
+ if io.respond_to?(:seek)
480
+ diff = @structure_start_pos - @in_pos
481
+ if diff < 0
482
+ begin
483
+ io.seek(diff, ::IO::SEEK_CUR)
484
+ @in_pos += diff
485
+ rescue IOError
486
+ end
487
+ end
488
+ end
489
+ end
490
+
491
+ # Calls {Libbz2::BZ2_bzDecompressInit} to initialize the decompression
492
+ # stream `s`.
493
+ #
494
+ # Defines a finalizer to ensure that the memory associated with the stream
495
+ # is deallocated.
496
+ #
497
+ # @param s [Libbz2::BzStream] The stream to initialize decompression for.
498
+ # @raise [Error::Bzip2Error] If {Libbz2::BZ2_bzDecompressInit} reports an
499
+ # error.
500
+ def decompress_init(s)
501
+ check_error(Libbz2::BZ2_bzDecompressInit(s, 0, @small))
502
+
503
+ ObjectSpace.define_finalizer(self, self.class.send(:finalize, s))
405
504
  end
406
505
 
407
- # Calls BZ2_bzDecompressEnd to release memeory associated with the
408
- # decompression stream `s`.
506
+ # Calls {Libbz2::BZ2_bzDecompressEnd} to release memory associated with
507
+ # the decompression stream `s`.
409
508
  #
410
509
  # Notifies `ObjectSpace` that it is no longer necessary to finalize the
411
- # `Reader` instance.
510
+ # {Reader} instance.
412
511
  #
413
512
  # @param s [Libbz2::BzStream] The stream to end decompression for.
414
- # @raise [Error::Bzip2Error] If `BZ2_bzDecompressEnd` reports an error.
513
+ # @raise [Error::Bzip2Error] If {Libbz2::BZ2_bzDecompressEnd} reports an
514
+ # error.
415
515
  def decompress_end(s)
416
516
  res = Libbz2::BZ2_bzDecompressEnd(s)
417
517
  ObjectSpace.undefine_finalizer(self)