burli 0.1.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.
@@ -0,0 +1,456 @@
1
+ //! Helpers over rb-sys, after the ones in the zrip and lz4rip gems.
2
+ //!
3
+ //! Ruby calls that can raise run under `rb_protect`, so Ruby never jumps
4
+ //! over Rust frames. Entry points catch panics and raise Ruby errors only
5
+ //! once all Rust values are dropped.
6
+
7
+ use std::ffi::{CStr, CString, c_char, c_int, c_long, c_void};
8
+ use std::panic::{AssertUnwindSafe, catch_unwind};
9
+ use std::ptr;
10
+
11
+ use rb_sys::{VALUE, rb_data_type_t};
12
+
13
+ pub type RbResult<T = VALUE> = Result<T, RubyErr>;
14
+
15
+ #[derive(Debug)]
16
+ pub enum RubyErr {
17
+ /// A Ruby exception or throw that `rb_protect` caught.
18
+ Jump(c_int),
19
+ Error {
20
+ class: VALUE,
21
+ message: String,
22
+ },
23
+ }
24
+
25
+ impl RubyErr {
26
+ pub fn new(class: VALUE, message: impl Into<String>) -> Self {
27
+ Self::Error {
28
+ class,
29
+ message: message.into(),
30
+ }
31
+ }
32
+
33
+ pub fn arg(message: impl Into<String>) -> Self {
34
+ // SAFETY: Ruby sets the class globals before it loads extensions.
35
+ Self::new(unsafe { rb_sys::rb_eArgError }, message)
36
+ }
37
+
38
+ pub fn runtime(message: impl Into<String>) -> Self {
39
+ // SAFETY: as above.
40
+ Self::new(unsafe { rb_sys::rb_eRuntimeError }, message)
41
+ }
42
+ }
43
+
44
+ /// Runs a method body; raises its error once `f` and its values are gone.
45
+ pub fn wrap<F>(f: F) -> VALUE
46
+ where
47
+ F: FnOnce() -> RbResult<VALUE>,
48
+ {
49
+ match catch_unwind(AssertUnwindSafe(f)) {
50
+ Ok(Ok(value)) => value,
51
+ Ok(Err(err)) => raise(err),
52
+ Err(_) => raise(RubyErr::runtime("native Rust panic")),
53
+ }
54
+ }
55
+
56
+ pub fn raise(err: RubyErr) -> ! {
57
+ match err {
58
+ // SAFETY: re-raises what `rb_protect` caught, with no Rust frames
59
+ // left to skip.
60
+ RubyErr::Jump(state) => unsafe { rb_sys::rb_jump_tag(state) },
61
+ RubyErr::Error { class, message } => {
62
+ let message = CString::new(message.replace('\0', "\\0")).unwrap_or_default();
63
+ // SAFETY: as above.
64
+ unsafe { rb_sys::rb_exc_raise(rb_sys::rb_exc_new_cstr(class, message.as_ptr())) }
65
+ }
66
+ }
67
+ }
68
+
69
+ struct ProtectData<F> {
70
+ func: Option<F>,
71
+ panicked: bool,
72
+ }
73
+
74
+ /// Runs `func`, which calls Ruby, and returns what it raised as an error.
75
+ pub fn protect<F>(func: F) -> RbResult<VALUE>
76
+ where
77
+ F: FnOnce() -> VALUE,
78
+ {
79
+ unsafe extern "C" fn call<F: FnOnce() -> VALUE>(arg: VALUE) -> VALUE {
80
+ // SAFETY: `arg` is the `ProtectData` below, which outlives this call.
81
+ let data = unsafe { &mut *(arg as *mut ProtectData<F>) };
82
+ if let Some(Ok(value)) = data
83
+ .func
84
+ .take()
85
+ .map(|func| catch_unwind(AssertUnwindSafe(func)))
86
+ {
87
+ return value;
88
+ }
89
+ data.panicked = true;
90
+ qnil()
91
+ }
92
+
93
+ let mut data = ProtectData {
94
+ func: Some(func),
95
+ panicked: false,
96
+ };
97
+ let mut state = 0;
98
+ // SAFETY: `call` gets a pointer to `data`, which lives across the call.
99
+ let value =
100
+ unsafe { rb_sys::rb_protect(Some(call::<F>), &raw mut data as VALUE, &raw mut state) };
101
+ if state != 0 {
102
+ Err(RubyErr::Jump(state))
103
+ } else if data.panicked {
104
+ Err(RubyErr::runtime("native Rust panic"))
105
+ } else {
106
+ Ok(value)
107
+ }
108
+ }
109
+
110
+ #[cfg(ruby_engine = "mri")]
111
+ struct WithoutGvl<F, R> {
112
+ func: Option<F>,
113
+ output: Option<std::thread::Result<R>>,
114
+ }
115
+
116
+ /// Runs `func` without the GVL, so other threads run meanwhile. `func`
117
+ /// must not call Ruby.
118
+ ///
119
+ /// `rb_thread_call_without_gvl2` neither raises nor checks interrupts, so an
120
+ /// interrupt cannot drop the output of work that changed native state. If
121
+ /// an interrupt is pending, `func` runs with the GVL, and Ruby handles the
122
+ /// interrupt when the method returns.
123
+ #[cfg(ruby_engine = "mri")]
124
+ pub fn without_gvl<F, R>(func: F) -> RbResult<R>
125
+ where
126
+ F: FnOnce() -> R,
127
+ {
128
+ unsafe extern "C" fn call<F: FnOnce() -> R, R>(data: *mut c_void) -> *mut c_void {
129
+ // SAFETY: `data` is the `WithoutGvl` below, which outlives this call.
130
+ let data = unsafe { &mut *data.cast::<WithoutGvl<F, R>>() };
131
+ if let Some(func) = data.func.take() {
132
+ // Panics must not unwind into Ruby.
133
+ data.output = Some(catch_unwind(AssertUnwindSafe(func)));
134
+ }
135
+ ptr::null_mut()
136
+ }
137
+
138
+ let mut data = WithoutGvl {
139
+ func: Some(func),
140
+ output: None,
141
+ };
142
+ // SAFETY: `call` touches only `data`, which lives across the call.
143
+ unsafe {
144
+ rb_sys::rb_thread_call_without_gvl2(
145
+ Some(call::<F, R>),
146
+ (&raw mut data).cast(),
147
+ None,
148
+ ptr::null_mut(),
149
+ );
150
+ }
151
+ let output = match (data.output, data.func) {
152
+ (Some(output), _) => output,
153
+ (None, Some(func)) => catch_unwind(AssertUnwindSafe(func)),
154
+ (None, None) => return Err(RubyErr::runtime("without-GVL work did not run")),
155
+ };
156
+ output.map_err(|_| RubyErr::runtime("native Rust panic"))
157
+ }
158
+
159
+ /// Runs `func` without the GVL when `release_gvl`. `TruffleRuby` has no GVL
160
+ /// to release.
161
+ pub fn maybe_without_gvl<F, R>(release_gvl: bool, func: F) -> RbResult<R>
162
+ where
163
+ F: FnOnce() -> R,
164
+ {
165
+ #[cfg(ruby_engine = "mri")]
166
+ if release_gvl {
167
+ return without_gvl(func);
168
+ }
169
+ let _ = release_gvl;
170
+ catch_unwind(AssertUnwindSafe(func)).map_err(|_| RubyErr::runtime("native Rust panic"))
171
+ }
172
+
173
+ pub const fn qnil() -> VALUE {
174
+ rb_sys::ruby_special_consts::RUBY_Qnil as VALUE
175
+ }
176
+
177
+ pub const fn qtrue() -> VALUE {
178
+ rb_sys::ruby_special_consts::RUBY_Qtrue as VALUE
179
+ }
180
+
181
+ pub const fn qfalse() -> VALUE {
182
+ rb_sys::ruby_special_consts::RUBY_Qfalse as VALUE
183
+ }
184
+
185
+ pub const fn bool_value(value: bool) -> VALUE {
186
+ if value { qtrue() } else { qfalse() }
187
+ }
188
+
189
+ /// A new ASCII-8BIT string with `bytes`.
190
+ pub fn new_binary_string(bytes: &[u8]) -> RbResult<VALUE> {
191
+ let len = c_long::try_from(bytes.len()).map_err(|_| RubyErr::arg("length too large"))?;
192
+ // SAFETY: `bytes` is valid for `len` bytes; Ruby copies them.
193
+ protect(|| unsafe { rb_sys::rb_str_new(bytes.as_ptr().cast::<c_char>(), len) })
194
+ }
195
+
196
+ /// `value` as a String, through `to_str`.
197
+ fn string_value(value: VALUE) -> RbResult<VALUE> {
198
+ // SAFETY: `rb_str_to_str` takes any object and raises on failure.
199
+ protect(|| unsafe { rb_sys::rb_str_to_str(value) })
200
+ }
201
+
202
+ /// The bytes of `string`, valid while Ruby cannot change or free it.
203
+ ///
204
+ /// # Safety
205
+ ///
206
+ /// `string` must be a String.
207
+ unsafe fn string_bytes<'a>(string: VALUE) -> &'a [u8] {
208
+ // SAFETY: the caller passes a String.
209
+ let (data, len) = unsafe { (rb_sys::RSTRING_PTR(string), rb_sys::RSTRING_LEN(string)) };
210
+ match usize::try_from(len) {
211
+ Ok(len) if len > 0 && !data.is_null() => {
212
+ // SAFETY: a String holds `len` bytes at `data`.
213
+ unsafe { std::slice::from_raw_parts(data.cast::<u8>(), len) }
214
+ }
215
+ _ => &[],
216
+ }
217
+ }
218
+
219
+ /// A copy of the bytes of `value`, through `to_str`.
220
+ pub fn value_to_bytes(value: VALUE) -> RbResult<Vec<u8>> {
221
+ let string = string_value(value)?;
222
+ // SAFETY: `string` is a String, read before Ruby runs again.
223
+ Ok(unsafe { string_bytes(string) }.to_vec())
224
+ }
225
+
226
+ pub fn value_to_option_bytes(value: VALUE) -> RbResult<Option<Vec<u8>>> {
227
+ if value == qnil() {
228
+ return Ok(None);
229
+ }
230
+ value_to_bytes(value).map(Some)
231
+ }
232
+
233
+ /// Input bytes, borrowed from a String where Ruby allows it.
234
+ pub enum InputBytes {
235
+ #[cfg(ruby_engine = "mri")]
236
+ Borrowed(BorrowedString),
237
+ Owned(Vec<u8>),
238
+ }
239
+
240
+ impl InputBytes {
241
+ pub fn as_slice(&self) -> &[u8] {
242
+ match self {
243
+ #[cfg(ruby_engine = "mri")]
244
+ Self::Borrowed(string) => string.as_slice(),
245
+ Self::Owned(bytes) => bytes,
246
+ }
247
+ }
248
+
249
+ pub fn len(&self) -> usize {
250
+ self.as_slice().len()
251
+ }
252
+
253
+ /// Keeps other threads from changing the bytes while the GVL is
254
+ /// released. Copies them when the String cannot be locked, as while
255
+ /// another thread works on it.
256
+ pub fn lock(&mut self) {
257
+ match self {
258
+ #[cfg(ruby_engine = "mri")]
259
+ Self::Borrowed(string) => {
260
+ if !string.lock() {
261
+ let bytes = string.as_slice().to_vec();
262
+ *self = Self::Owned(bytes);
263
+ }
264
+ }
265
+ Self::Owned(_) => {}
266
+ }
267
+ }
268
+ }
269
+
270
+ #[cfg(ruby_engine = "mri")]
271
+ pub struct BorrowedString {
272
+ value: VALUE,
273
+ locked: bool,
274
+ }
275
+
276
+ #[cfg(ruby_engine = "mri")]
277
+ impl BorrowedString {
278
+ fn as_slice(&self) -> &[u8] {
279
+ // SAFETY: `value` is a String. While the GVL is held, only this
280
+ // thread runs; without it, the String is frozen or locked.
281
+ unsafe { string_bytes(self.value) }
282
+ }
283
+
284
+ /// Whether the String is frozen or now locked.
285
+ fn lock(&mut self) -> bool {
286
+ // SAFETY: `value` is a String.
287
+ if self.locked || unsafe { rb_sys::rb_obj_frozen_p(self.value) } == qtrue() {
288
+ return true;
289
+ }
290
+ // SAFETY: raises if the String is already locked, which `protect`
291
+ // catches.
292
+ if protect(|| unsafe { rb_sys::rb_str_locktmp(self.value) }).is_err() {
293
+ // SAFETY: drops the caught error, so it does not show as `$!`.
294
+ unsafe { rb_sys::rb_set_errinfo(qnil()) };
295
+ return false;
296
+ }
297
+ self.locked = true;
298
+ true
299
+ }
300
+ }
301
+
302
+ #[cfg(ruby_engine = "mri")]
303
+ impl Drop for BorrowedString {
304
+ fn drop(&mut self) {
305
+ if self.locked {
306
+ // SAFETY: undoes the `rb_str_locktmp` above.
307
+ unsafe { rb_sys::rb_str_unlocktmp(self.value) };
308
+ }
309
+ // Keeps the String alive until here.
310
+ let _ = rb_sys::rb_gc_guard!(self.value);
311
+ }
312
+ }
313
+
314
+ /// The bytes of `value`, through `to_str`. `TruffleRuby` gets a copy.
315
+ pub fn input_bytes(value: VALUE) -> RbResult<InputBytes> {
316
+ #[cfg(ruby_engine = "mri")]
317
+ {
318
+ let value = string_value(value)?;
319
+ Ok(InputBytes::Borrowed(BorrowedString {
320
+ value,
321
+ locked: false,
322
+ }))
323
+ }
324
+ #[cfg(not(ruby_engine = "mri"))]
325
+ {
326
+ value_to_bytes(value).map(InputBytes::Owned)
327
+ }
328
+ }
329
+
330
+ pub fn value_to_i64(value: VALUE) -> RbResult<i64> {
331
+ let mut out = 0;
332
+ // SAFETY: raises for non-Integers, which `protect` catches.
333
+ protect(|| {
334
+ out = unsafe { rb_sys::rb_num2ll(value) };
335
+ qnil()
336
+ })?;
337
+ Ok(out)
338
+ }
339
+
340
+ /// Wraps `value` in a new object of `class`; drops it if that fails.
341
+ ///
342
+ /// # Safety
343
+ ///
344
+ /// `data_type` must free a `Box<T>`.
345
+ pub unsafe fn wrap_typed_data<T>(
346
+ class: VALUE,
347
+ value: Box<T>,
348
+ data_type: *const rb_data_type_t,
349
+ ) -> RbResult<VALUE> {
350
+ let raw = Box::into_raw(value);
351
+ // SAFETY: Ruby owns `raw` once the object exists.
352
+ let wrapped = protect(|| unsafe {
353
+ rb_sys::rb_data_typed_object_wrap(class, raw.cast::<c_void>(), data_type)
354
+ });
355
+ if wrapped.is_err() {
356
+ // SAFETY: no object took `raw`.
357
+ drop(unsafe { Box::from_raw(raw) });
358
+ }
359
+ wrapped
360
+ }
361
+
362
+ /// The `T` that `value` wraps.
363
+ ///
364
+ /// # Safety
365
+ ///
366
+ /// `data_type` must wrap a `T`. The reference must not outlive `value`.
367
+ pub unsafe fn typed_data_ref<'a, T>(
368
+ value: VALUE,
369
+ data_type: *const rb_data_type_t,
370
+ ) -> RbResult<&'a T> {
371
+ let mut data = ptr::null_mut();
372
+ // SAFETY: raises a TypeError for other objects, which `protect` catches.
373
+ protect(|| {
374
+ data = unsafe { rb_sys::rb_check_typeddata(value, data_type) };
375
+ qnil()
376
+ })?;
377
+ if data.is_null() {
378
+ return Err(RubyErr::runtime("object has no native data"));
379
+ }
380
+ // SAFETY: the caller's contract above.
381
+ Ok(unsafe { &*data.cast::<T>() })
382
+ }
383
+
384
+ pub fn define_module(name: &CStr) -> RbResult<VALUE> {
385
+ // SAFETY: `name` is a C string.
386
+ protect(|| unsafe { rb_sys::rb_define_module(name.as_ptr()) })
387
+ }
388
+
389
+ pub fn define_class_under(outer: VALUE, name: &CStr, superclass: VALUE) -> RbResult<VALUE> {
390
+ // SAFETY: as above.
391
+ protect(|| unsafe { rb_sys::rb_define_class_under(outer, name.as_ptr(), superclass) })
392
+ }
393
+
394
+ /// Classes whose objects only native constructors make.
395
+ pub fn undef_alloc_func(class: VALUE) -> RbResult<()> {
396
+ // SAFETY: `class` is a class.
397
+ protect(|| {
398
+ unsafe { rb_sys::rb_undef_alloc_func(class) };
399
+ qnil()
400
+ })?;
401
+ Ok(())
402
+ }
403
+
404
+ /// A method body that takes the receiver and `ARITY` arguments.
405
+ pub trait Method {
406
+ const ARITY: c_int;
407
+ fn erase(self) -> unsafe extern "C" fn() -> VALUE;
408
+ }
409
+
410
+ macro_rules! method_arity {
411
+ ($arity:literal $(, $arg:ty)*) => {
412
+ impl Method for unsafe extern "C" fn(VALUE $(, $arg)*) -> VALUE {
413
+ const ARITY: c_int = $arity;
414
+
415
+ fn erase(self) -> unsafe extern "C" fn() -> VALUE {
416
+ // SAFETY: Ruby calls a method of arity N with the receiver
417
+ // and N arguments, as this type takes.
418
+ unsafe { std::mem::transmute::<Self, unsafe extern "C" fn() -> VALUE>(self) }
419
+ }
420
+ }
421
+ };
422
+ }
423
+
424
+ method_arity!(0);
425
+ method_arity!(1, VALUE);
426
+ method_arity!(2, VALUE, VALUE);
427
+ method_arity!(3, VALUE, VALUE, VALUE);
428
+ method_arity!(4, VALUE, VALUE, VALUE, VALUE);
429
+ method_arity!(5, VALUE, VALUE, VALUE, VALUE, VALUE);
430
+
431
+ pub fn define_method<M: Method>(class: VALUE, name: &CStr, func: M) -> RbResult<()> {
432
+ let func = func.erase();
433
+ // SAFETY: `func` takes the arguments `M::ARITY` says.
434
+ protect(|| {
435
+ unsafe { rb_sys::rb_define_method(class, name.as_ptr(), Some(func), M::ARITY) };
436
+ qnil()
437
+ })?;
438
+ Ok(())
439
+ }
440
+
441
+ pub fn define_singleton_method<M: Method>(object: VALUE, name: &CStr, func: M) -> RbResult<()> {
442
+ let func = func.erase();
443
+ // SAFETY: as above.
444
+ protect(|| {
445
+ unsafe { rb_sys::rb_define_singleton_method(object, name.as_ptr(), Some(func), M::ARITY) };
446
+ qnil()
447
+ })?;
448
+ Ok(())
449
+ }
450
+
451
+ /// Method body types, for `define_method`.
452
+ pub type Method0 = unsafe extern "C" fn(VALUE) -> VALUE;
453
+ pub type Method1 = unsafe extern "C" fn(VALUE, VALUE) -> VALUE;
454
+ pub type Method2 = unsafe extern "C" fn(VALUE, VALUE, VALUE) -> VALUE;
455
+ pub type Method4 = unsafe extern "C" fn(VALUE, VALUE, VALUE, VALUE, VALUE) -> VALUE;
456
+ pub type Method5 = unsafe extern "C" fn(VALUE, VALUE, VALUE, VALUE, VALUE, VALUE) -> VALUE;
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Burli
4
+ # Decompresses a stream read from an IO.
5
+ class Reader
6
+ # Compressed bytes read from the IO at a time, and the least decoded
7
+ # output each step asks for.
8
+ CHUNK_SIZE = 16 * 1024
9
+
10
+ # @param io [#read, #readpartial] where the compressed stream comes from
11
+ # @param options [Hash, nil] +dictionary+, as for {Burli.inflate}
12
+ def initialize(io, options = nil, **kwargs)
13
+ raise ArgumentError, "io should not be nil" if io.nil?
14
+
15
+ @io = io
16
+ @decompressor = Decompressor.new(Hash(options).merge(kwargs))
17
+ @buffer = "".b
18
+ @closed = false
19
+ end
20
+
21
+ # Reads +length+ decoded bytes, or all of them without +length+.
22
+ # @return [String, nil] nil at the end of the stream when +length+ is given
23
+ def read(length = nil, outbuf = nil)
24
+ ensure_open
25
+ if length.nil?
26
+ step(nil) until @decompressor.finished?
27
+ return output(outbuf, @buffer.bytesize)
28
+ end
29
+
30
+ length = Integer(length)
31
+ raise ArgumentError, "negative length #{length} given" if length.negative?
32
+ return output(outbuf, 0) if length.zero?
33
+
34
+ step(length - @buffer.bytesize) while @buffer.bytesize < length && !@decompressor.finished?
35
+ if @buffer.empty?
36
+ outbuf&.clear
37
+ return nil
38
+ end
39
+ output(outbuf, length)
40
+ end
41
+
42
+ # Reads at most +maxlen+ decoded bytes, waiting only until some arrive.
43
+ # @raise [EOFError] at the end of the stream
44
+ def readpartial(maxlen, outbuf = nil)
45
+ ensure_open
46
+ maxlen = Integer(maxlen)
47
+ raise ArgumentError, "max length must be positive" unless maxlen.positive?
48
+
49
+ step(maxlen) while @buffer.empty? && !@decompressor.finished?
50
+ if @buffer.empty?
51
+ outbuf&.clear
52
+ raise EOFError, "end of file reached"
53
+ end
54
+ output(outbuf, maxlen)
55
+ end
56
+
57
+ def eof?
58
+ ensure_open
59
+ step(1) while @buffer.empty? && !@decompressor.finished?
60
+ @buffer.empty?
61
+ end
62
+
63
+ # Closes the IO.
64
+ # @return [IO] the IO
65
+ def close
66
+ return @io if @closed
67
+
68
+ begin
69
+ @io.close if @io.respond_to?(:close)
70
+ ensure
71
+ @closed = true
72
+ @buffer.clear
73
+ end
74
+ @io
75
+ end
76
+
77
+ def closed?
78
+ @closed
79
+ end
80
+
81
+ private
82
+
83
+ def ensure_open
84
+ raise Error, "Reader is closed" if @closed
85
+ end
86
+
87
+ # Decodes into the buffer, at least +wanted+ bytes per step unless the
88
+ # input ends first. Reads input only when the decompressor takes it.
89
+ def step(wanted)
90
+ input = "".b
91
+ if @decompressor.can_accept_more_data
92
+ input = read_input
93
+ raise Error, "unexpected end of compressed stream" if input.nil? || input.empty?
94
+ end
95
+ limit = wanted && [wanted, CHUNK_SIZE].max
96
+ @buffer << @decompressor.process(input, output_buffer_limit: limit)
97
+ end
98
+
99
+ def read_input
100
+ return @io.read(CHUNK_SIZE) unless @io.respond_to?(:readpartial)
101
+
102
+ @io.readpartial(CHUNK_SIZE)
103
+ rescue EOFError
104
+ nil
105
+ end
106
+
107
+ # Takes up to +length+ bytes from the buffer.
108
+ def output(outbuf, length)
109
+ data = length >= @buffer.bytesize ? @buffer : @buffer.byteslice(0, length)
110
+ @buffer = length >= @buffer.bytesize ? "".b : @buffer.byteslice(length..)
111
+ return data unless outbuf
112
+
113
+ outbuf.replace(data)
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Burli
4
+ # The gem version.
5
+ VERSION = "0.1.0"
6
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Burli
4
+ # Compresses what is written to it into an IO.
5
+ class Writer
6
+ # @param io [#write] where the compressed stream goes
7
+ # @param options [Hash, nil] +mode+, +quality+, +lgwin+, and +lgblock+, as
8
+ # for {Burli.deflate}
9
+ def initialize(io, options = nil, **kwargs)
10
+ raise ArgumentError, "io should not be nil" if io.nil?
11
+
12
+ @io = io
13
+ @compressor = Compressor.new(Hash(options).merge(kwargs))
14
+ @closed = false
15
+ end
16
+
17
+ # Compresses +data+ and writes the output produced so far.
18
+ # @return [Integer] the bytes of +data+
19
+ def write(data)
20
+ ensure_open
21
+ data = String.try_convert(data) || raise(TypeError, "no implicit conversion of #{data.class} into String")
22
+ put(@compressor.process(data))
23
+ data.bytesize
24
+ end
25
+
26
+ # Writes output that decodes to all data so far, and flushes the IO.
27
+ # @return [self]
28
+ def flush
29
+ ensure_open
30
+ put(@compressor.flush)
31
+ @io.flush if @io.respond_to?(:flush)
32
+ self
33
+ end
34
+
35
+ # Ends the stream without closing the IO.
36
+ # @return [IO] the IO
37
+ def finish
38
+ ensure_open
39
+ put(@compressor.finish)
40
+ @io
41
+ end
42
+
43
+ # Ends the stream and closes the IO.
44
+ # @return [IO] the IO
45
+ def close
46
+ return @io if @closed
47
+
48
+ begin
49
+ finish
50
+ ensure
51
+ @closed = true
52
+ @io.close if @io.respond_to?(:close)
53
+ end
54
+ @io
55
+ end
56
+
57
+ def closed?
58
+ @closed
59
+ end
60
+
61
+ private
62
+
63
+ def ensure_open
64
+ raise Error, "Writer is closed" if @closed
65
+ end
66
+
67
+ def put(output)
68
+ @io.write(output) unless output.empty?
69
+ end
70
+ end
71
+ end