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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +11 -0
- data/README.md +29 -0
- data/ext/burli_native/Cargo.toml +25 -0
- data/ext/burli_native/build.rs +24 -0
- data/ext/burli_native/extconf.rb +14 -0
- data/ext/burli_native/src/lib.rs +561 -0
- data/ext/burli_native/src/rb.rs +456 -0
- data/lib/burli/reader.rb +116 -0
- data/lib/burli/version.rb +6 -0
- data/lib/burli/writer.rb +71 -0
- data/lib/burli.rb +137 -0
- metadata +68 -0
|
@@ -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;
|
data/lib/burli/reader.rb
ADDED
|
@@ -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
|
data/lib/burli/writer.rb
ADDED
|
@@ -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
|