zrip 0.2.0 → 0.2.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 50caa1679bd74290e01032b2564ab2ac083633f00ad7070f2e3e597572ffe22b
4
- data.tar.gz: d89a94b7221aba11a0084f8ffeab771428f682ddf19908a647c2bce862b43ffe
3
+ metadata.gz: 8a16cab574ece332812f7e25c48b75152a11c63418619527273e974353c7279b
4
+ data.tar.gz: 9504303323b72ca6b079ddcb528a3be99d550a56125241c669613221d5aea4e7
5
5
  SHA512:
6
- metadata.gz: 74bc8d13edae353144da41a66e80d41440157dc11c9555726c2c01659d88a02c8ffd0a7410b0157fb011a45677c54448a808ed463a1f9b1d99bd9a2769e42926
7
- data.tar.gz: 91c282a2f5275e2e0cd72a9a2cf089a0582e47d256acce3c9bf67be6f494ab89a046bfece175d23f9543e7162b90b8bf66cc0dd97e5ebd5c9a695c3462e23d01
6
+ metadata.gz: f80dfc72c92ce4a072d186ba75efa88f88281aab64cc94a19d15e3e15f1548aa52fa7b9cec4d7593db1dc6b4a8ac37cf28e37d14cb38beb059d85f7b6c460288
7
+ data.tar.gz: 910c850499f28d49c02fdfca756770a4d182ac7688891efa7cccc920f1dd2180b30fbcb984cf8b34e143d049374cd0d6da947f5f8f55998a89d9183753a17c9b
data/CHANGELOG.md CHANGED
@@ -2,6 +2,27 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.2.1] - 2026-10-11
6
+
7
+ - Update the `zrip` crate dependency to 0.8.11 and `rb-sys` to 0.9.130.
8
+ - Make `FrameCodec` instances Ractor-shareable as documented, with native
9
+ dictionary copies that leave caller strings mutable.
10
+ - Unlock input strings and free native results when Ruby interrupts a call
11
+ that released the GVL.
12
+ - Protect mutable inputs when mutex contention releases the GVL, including
13
+ calls below the normal size threshold.
14
+ - Catch Rust panics inside native callbacks instead of aborting the process.
15
+ - Preserve Ruby exceptions, `Thread#kill`, and `catch`/`throw` through
16
+ protected native calls.
17
+ - Avoid trainer deadlocks during sample coercion and borrow decompression
18
+ input after size coercion.
19
+ - Enforce `max_output_size:` inside the native decoder across concatenated
20
+ frames, before allocating or decoding past the limit.
21
+ - Validate complete frame headers when reading frame content sizes.
22
+ - Correct the README compression level example to the supported maximum.
23
+ - Update GitHub Actions checkout to v7 and check Rust tests, formatting, and
24
+ Clippy in CI.
25
+
5
26
  ## [0.2.0] - 2026-08-07
6
27
 
7
28
  - Release the Ruby GVL around large frame/block compression and frame
data/Cargo.lock CHANGED
@@ -31,9 +31,9 @@ dependencies = [
31
31
 
32
32
  [[package]]
33
33
  name = "bitflags"
34
- version = "2.13.1"
34
+ version = "2.13.2"
35
35
  source = "registry+https://github.com/rust-lang/crates.io-index"
36
- checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
36
+ checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
37
37
 
38
38
  [[package]]
39
39
  name = "cexpr"
@@ -46,9 +46,9 @@ dependencies = [
46
46
 
47
47
  [[package]]
48
48
  name = "cfg-if"
49
- version = "1.0.4"
49
+ version = "1.0.5"
50
50
  source = "registry+https://github.com/rust-lang/crates.io-index"
51
- checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
51
+ checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
52
52
 
53
53
  [[package]]
54
54
  name = "clang-sys"
@@ -63,15 +63,15 @@ dependencies = [
63
63
 
64
64
  [[package]]
65
65
  name = "either"
66
- version = "1.17.0"
66
+ version = "1.19.0"
67
67
  source = "registry+https://github.com/rust-lang/crates.io-index"
68
- checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d"
68
+ checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be"
69
69
 
70
70
  [[package]]
71
71
  name = "fearless_simd"
72
- version = "0.6.0"
72
+ version = "1.1.0"
73
73
  source = "registry+https://github.com/rust-lang/crates.io-index"
74
- checksum = "e0f5f895dac0865bc235f1364793899569a7db538137f1024dccea56bf41c78d"
74
+ checksum = "3296eb97d869113eaf148a6bdf01884e90b3828ad7ae2340b0f93362fc158611"
75
75
 
76
76
  [[package]]
77
77
  name = "glob"
@@ -90,15 +90,15 @@ dependencies = [
90
90
 
91
91
  [[package]]
92
92
  name = "lazy_static"
93
- version = "1.5.0"
93
+ version = "1.5.1"
94
94
  source = "registry+https://github.com/rust-lang/crates.io-index"
95
- checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
95
+ checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb"
96
96
 
97
97
  [[package]]
98
98
  name = "libc"
99
- version = "0.2.189"
99
+ version = "0.2.190"
100
100
  source = "registry+https://github.com/rust-lang/crates.io-index"
101
- checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
101
+ checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78"
102
102
 
103
103
  [[package]]
104
104
  name = "libloading"
@@ -152,18 +152,18 @@ dependencies = [
152
152
 
153
153
  [[package]]
154
154
  name = "rb-sys"
155
- version = "0.9.128"
155
+ version = "0.9.130"
156
156
  source = "registry+https://github.com/rust-lang/crates.io-index"
157
- checksum = "45ca28513560e56cfb79a62b1fce363c73af170a182024ce880c77ee9429920a"
157
+ checksum = "02faf625bb10ba893e3ae620f19c9fb1b5f8fcae0fe4eb86bb3f2230fad75edb"
158
158
  dependencies = [
159
159
  "rb-sys-build",
160
160
  ]
161
161
 
162
162
  [[package]]
163
163
  name = "rb-sys-build"
164
- version = "0.9.128"
164
+ version = "0.9.130"
165
165
  source = "registry+https://github.com/rust-lang/crates.io-index"
166
- checksum = "ce04b2c55eff3a21aaa623fcc655d94373238e72cac6b3e1a3641ff31649f99a"
166
+ checksum = "05be6f9c86fe5482808162826f6c047d093b3670582296c770de1d94f8066694"
167
167
  dependencies = [
168
168
  "bindgen",
169
169
  "lazy_static",
@@ -234,9 +234,9 @@ dependencies = [
234
234
 
235
235
  [[package]]
236
236
  name = "unicode-ident"
237
- version = "1.0.24"
237
+ version = "1.0.26"
238
238
  source = "registry+https://github.com/rust-lang/crates.io-index"
239
- checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
239
+ checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
240
240
 
241
241
  [[package]]
242
242
  name = "windows-link"
@@ -246,17 +246,17 @@ checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
246
246
 
247
247
  [[package]]
248
248
  name = "zrip"
249
- version = "0.1.2"
249
+ version = "0.2.1"
250
250
  dependencies = [
251
251
  "rb-sys",
252
- "zrip 0.8.5",
252
+ "zrip 0.8.11",
253
253
  ]
254
254
 
255
255
  [[package]]
256
256
  name = "zrip"
257
- version = "0.8.5"
257
+ version = "0.8.11"
258
258
  source = "registry+https://github.com/rust-lang/crates.io-index"
259
- checksum = "fa05c09787b3aaaaa57870dca9fd9f3b4934018cbb5dde1b112017e2e70ec491"
259
+ checksum = "9e85bd0b02784d8753fe5cc435dfb65043be505cb621643a3ac4a0c43eb49e4a"
260
260
  dependencies = [
261
261
  "zrip-core",
262
262
  "zrip-decode",
@@ -265,15 +265,15 @@ dependencies = [
265
265
 
266
266
  [[package]]
267
267
  name = "zrip-core"
268
- version = "0.9.1"
268
+ version = "0.11.0"
269
269
  source = "registry+https://github.com/rust-lang/crates.io-index"
270
- checksum = "9335acd94c1f09f729178fc339f9d49790254c1e98ba076f88df3a3f534c4e7d"
270
+ checksum = "6dec95be8c75252532ba0ef5e738d9da26a6f51ad52cb45f4ed90c89979727c1"
271
271
 
272
272
  [[package]]
273
273
  name = "zrip-decode"
274
- version = "0.8.5"
274
+ version = "0.8.10"
275
275
  source = "registry+https://github.com/rust-lang/crates.io-index"
276
- checksum = "1801cd8b5d374e2bc31cd8a231182dd47e621492fa0bf68ca7d3033e001cd06b"
276
+ checksum = "aca029cf83b6a6e282008a9aa1079e13f216b28bac670eb1fb0769c1d256cb0e"
277
277
  dependencies = [
278
278
  "fearless_simd",
279
279
  "zrip-core",
@@ -281,9 +281,10 @@ dependencies = [
281
281
 
282
282
  [[package]]
283
283
  name = "zrip-encode"
284
- version = "0.8.4"
284
+ version = "0.9.0"
285
285
  source = "registry+https://github.com/rust-lang/crates.io-index"
286
- checksum = "38e7e22b665b947f0240b44805713e87a15c34e86d0891cbe2856c575c5cda1d"
286
+ checksum = "b657fa83287fe6799596e53540a7b1f179ca34ffd8589d5c576a5b8a1bcc4260"
287
287
  dependencies = [
288
+ "fearless_simd",
288
289
  "zrip-core",
289
290
  ]
data/Cargo.toml CHANGED
@@ -6,4 +6,3 @@ resolver = "2"
6
6
  opt-level = 3
7
7
  lto = true
8
8
  codegen-units = 1
9
- panic = "abort"
data/README.md CHANGED
@@ -58,7 +58,7 @@ original = codec.decompress(compressed)
58
58
 
59
59
  ```ruby
60
60
  fast = Zrip::FrameCodec.new(level: -3) # negative = faster
61
- strong = Zrip::FrameCodec.new(level: 19) # higher = smaller output
61
+ strong = Zrip::FrameCodec.new(level: 4) # highest supported level
62
62
  ```
63
63
 
64
64
  ### Bounded decompression
data/ext/zrip/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "zrip"
3
- version = "0.1.2"
3
+ version = "0.2.1"
4
4
  edition = "2021"
5
5
  build = "build.rs"
6
6
 
@@ -9,8 +9,8 @@ name = "zrip"
9
9
  crate-type = ["cdylib", "rlib"]
10
10
 
11
11
  [dependencies]
12
- zstd = { version = "0.8.5", package = "zrip", features = ["frame", "dict_builder"] }
13
- rb-sys = "0.9"
12
+ zstd = { version = "0.8.11", package = "zrip", features = ["frame", "dict_builder"] }
13
+ rb-sys = "0.9.130"
14
14
 
15
15
  [build-dependencies]
16
- rb-sys = "0.9"
16
+ rb-sys = "0.9.130"
data/ext/zrip/src/lib.rs CHANGED
@@ -1,7 +1,6 @@
1
1
  mod rb;
2
2
 
3
3
  use std::ffi::c_void;
4
- use std::io::Read;
5
4
  use std::panic::{catch_unwind, AssertUnwindSafe};
6
5
  use std::sync::{Mutex, OnceLock, TryLockError};
7
6
 
@@ -18,7 +17,6 @@ static OUTPUT_SIZE_LIMIT_ERROR: OnceLock<GlobalValue> = OnceLock::new();
18
17
 
19
18
  const GVL_COMPRESS_THRESHOLD: usize = 64 * 1024;
20
19
  const GVL_FRAME_DECOMPRESS_THRESHOLD: usize = 64 * 1024;
21
- const DECOMPRESS_READ_CHUNK: usize = 16 * 1024;
22
20
 
23
21
  #[derive(Copy, Clone)]
24
22
  struct GlobalValue(VALUE);
@@ -52,27 +50,38 @@ fn should_release_frame_decompress_gvl(output_len: usize) -> bool {
52
50
  output_len >= GVL_FRAME_DECOMPRESS_THRESHOLD
53
51
  }
54
52
 
55
- fn with_mutex<T, R, F>(mutex: &Mutex<T>, release_gvl: bool, name: &str, func: F) -> RbResult<R>
53
+ fn with_mutex<T, R, F>(
54
+ mutex: &Mutex<T>,
55
+ release_gvl: bool,
56
+ name: &str,
57
+ input: &mut rb::InputBytes,
58
+ func: F,
59
+ ) -> RbResult<R>
56
60
  where
57
- F: FnOnce(&mut T) -> RbResult<R>,
61
+ F: FnOnce(&mut T, &[u8]) -> RbResult<R>,
58
62
  {
59
63
  if release_gvl {
64
+ input.lock_for_without_gvl(true)?;
60
65
  return rb::maybe_without_gvl(true, || {
61
66
  let mut guard = mutex
62
67
  .lock()
63
68
  .map_err(|_| RubyErr::runtime(format!("{name} mutex poisoned")))?;
64
- func(&mut guard)
65
- });
69
+ func(&mut guard, input.as_slice())
70
+ })?;
66
71
  }
67
72
 
68
73
  match mutex.try_lock() {
69
- Ok(mut guard) => func(&mut guard),
70
- Err(TryLockError::WouldBlock) => rb::maybe_without_gvl(true, || {
71
- let mut guard = mutex
72
- .lock()
73
- .map_err(|_| RubyErr::runtime(format!("{name} mutex poisoned")))?;
74
- func(&mut guard)
75
- }),
74
+ Ok(mut guard) => func(&mut guard, input.as_slice()),
75
+ Err(TryLockError::WouldBlock) => {
76
+ // Even a small input needs protection when contention releases GVL.
77
+ input.lock_for_without_gvl(true)?;
78
+ rb::maybe_without_gvl(true, || {
79
+ let mut guard = mutex
80
+ .lock()
81
+ .map_err(|_| RubyErr::runtime(format!("{name} mutex poisoned")))?;
82
+ func(&mut guard, input.as_slice())
83
+ })?
84
+ }
76
85
  Err(TryLockError::Poisoned(_)) => Err(RubyErr::runtime(format!("{name} mutex poisoned"))),
77
86
  }
78
87
  }
@@ -89,119 +98,36 @@ enum BoundedError {
89
98
  }
90
99
 
91
100
  fn parse_frame_content_size(input: &[u8]) -> Result<Option<u64>, BoundedError> {
92
- if input.len() < 5 {
93
- return Err(BoundedError::BadMagic);
94
- }
95
- if input[..4] != ZSTD_FRAME_MAGIC {
96
- return Err(BoundedError::BadMagic);
97
- }
98
- let fhd = input[4];
99
- let fcs_flag = (fhd >> 6) & 3;
100
- let single_segment = (fhd >> 5) & 1;
101
- let dict_id_flag = fhd & 3;
102
-
103
- let window_desc_size = if single_segment == 0 { 1usize } else { 0 };
104
- let dict_id_size = [0usize, 1, 2, 4][dict_id_flag as usize];
105
- let fcs_field_size = match fcs_flag {
106
- 0 => {
107
- if single_segment == 1 {
108
- 1usize
109
- } else {
110
- return Ok(None);
111
- }
112
- }
113
- 1 => 2,
114
- 2 => 4,
115
- 3 => 8,
116
- _ => unreachable!(),
117
- };
118
-
119
- let fcs_offset = 5 + window_desc_size + dict_id_size;
120
- if input.len() < fcs_offset + fcs_field_size {
121
- return Err(BoundedError::BadMagic);
122
- }
123
-
124
- let fcs_bytes = &input[fcs_offset..fcs_offset + fcs_field_size];
125
- let value = match fcs_field_size {
126
- 1 => fcs_bytes[0] as u64,
127
- 2 => u16::from_le_bytes([fcs_bytes[0], fcs_bytes[1]]) as u64 + 256,
128
- 4 => u32::from_le_bytes([fcs_bytes[0], fcs_bytes[1], fcs_bytes[2], fcs_bytes[3]]) as u64,
129
- 8 => u64::from_le_bytes(fcs_bytes.try_into().unwrap()),
130
- _ => unreachable!(),
131
- };
132
-
133
- Ok(Some(value))
134
- }
135
-
136
- fn io_error_is_output_too_small(err: &std::io::Error) -> bool {
137
- matches!(
138
- err.get_ref()
139
- .and_then(|inner| inner.downcast_ref::<zstd::error::DecompressError>()),
140
- Some(zstd::error::DecompressError::OutputTooSmall)
141
- )
142
- }
143
-
144
- fn read_decoder_bounded<R: Read>(
145
- decoder: &mut R,
146
- max_output: usize,
147
- ) -> Result<Vec<u8>, BoundedError> {
148
- let mut output = Vec::new();
149
- let mut buf = [0u8; DECOMPRESS_READ_CHUNK];
150
-
151
- loop {
152
- let n = decoder.read(&mut buf).map_err(|e| {
153
- if io_error_is_output_too_small(&e) {
154
- BoundedError::OutputSizeLimit {
155
- limit: max_output as u64,
156
- }
157
- } else {
158
- BoundedError::DecoderFailed(format!("{e}"))
159
- }
160
- })?;
161
- if n == 0 {
162
- return Ok(output);
163
- }
164
-
165
- let Some(next_len) = output.len().checked_add(n) else {
166
- return Err(BoundedError::OutputSizeLimit {
167
- limit: max_output as u64,
168
- });
169
- };
170
- if next_len > max_output {
171
- return Err(BoundedError::OutputSizeLimit {
172
- limit: max_output as u64,
173
- });
174
- }
175
-
176
- output.extend_from_slice(&buf[..n]);
177
- }
101
+ zstd::frame::header::parse_frame_header(input)
102
+ .map(|header| header.frame_content_size)
103
+ .map_err(|err| match err {
104
+ zstd::DecompressError::BadMagic => BoundedError::BadMagic,
105
+ _ => BoundedError::DecoderFailed(err.to_string()),
106
+ })
178
107
  }
179
108
 
180
109
  fn decompress_bounded(
181
110
  compressed: &[u8],
182
111
  max_output: usize,
183
112
  dctx: &mut DecompressContext,
184
- dict: Option<&Dictionary>,
185
113
  ) -> Result<Vec<u8>, BoundedError> {
186
114
  if compressed.len() < ZSTD_FRAME_MAGIC.len() || compressed[..4] != ZSTD_FRAME_MAGIC {
187
115
  return Err(BoundedError::BadMagic);
188
116
  }
189
117
 
190
- let result = if max_output == 0 {
191
- dctx.decompress_with_limit(compressed, usize::MAX)
192
- .map(|out| out.into_owned())
193
- .map_err(|e| BoundedError::DecoderFailed(format!("{e}")))?
118
+ let limit = if max_output == 0 {
119
+ usize::MAX
194
120
  } else {
195
- let mut decoder = match dict {
196
- Some(dict) => {
197
- zstd::FrameDecoder::with_dict_and_limit(compressed, dict.clone(), usize::MAX)
198
- }
199
- None => zstd::FrameDecoder::with_limit(compressed, usize::MAX),
200
- };
201
- read_decoder_bounded(&mut decoder, max_output)?
121
+ max_output
202
122
  };
203
-
204
- Ok(result)
123
+ dctx.decompress_with_limit(compressed, limit)
124
+ .map(|out| out.into_owned())
125
+ .map_err(|err| match err {
126
+ zstd::DecompressError::OutputTooSmall => BoundedError::OutputSizeLimit {
127
+ limit: max_output as u64,
128
+ },
129
+ _ => BoundedError::DecoderFailed(err.to_string()),
130
+ })
205
131
  }
206
132
 
207
133
  fn decompress_work_size(compressed: &[u8], max_output: usize) -> usize {
@@ -277,7 +203,7 @@ fn make_frame_codec_data_type() -> rb_data_type_t {
277
203
  },
278
204
  parent: std::ptr::null(),
279
205
  data: std::ptr::null_mut(),
280
- flags: 1,
206
+ flags: rb::frozen_shareable_flags(),
281
207
  }
282
208
  }
283
209
 
@@ -372,7 +298,6 @@ unsafe fn dict_trainer_ref(value: VALUE) -> RbResult<&'static RbDictTrainer> {
372
298
  struct FrameCodec {
373
299
  dict_len: usize,
374
300
  dict_id: Option<u32>,
375
- dict: Option<Dictionary>,
376
301
  level: i32,
377
302
  cctx: Mutex<CompressContext>,
378
303
  dctx: Mutex<DecompressContext>,
@@ -382,16 +307,10 @@ unsafe impl Send for FrameCodec {}
382
307
  unsafe impl Sync for FrameCodec {}
383
308
 
384
309
  fn frame_codec_new_impl(class: VALUE, rb_dict: VALUE, id: VALUE, level: VALUE) -> RbResult<VALUE> {
385
- let rb_dict = if rb_dict == rb::qnil() {
386
- None
387
- } else {
388
- let rb_dict = rb::string_value(rb_dict)?;
389
- rb::freeze_value(rb_dict)?;
390
- Some(rb::value_to_bytes(rb_dict)?)
391
- };
310
+ let rb_dict = rb::value_to_option_bytes(rb_dict)?;
392
311
  let id = rb::value_to_u32(id)?;
393
312
  let level = rb::value_to_i32(level)?;
394
- let (dict_len, dict_id, dict, cctx, dctx) = match rb_dict {
313
+ let (dict_len, dict_id, cctx, dctx) = match rb_dict {
395
314
  None => {
396
315
  let cctx = CompressContext::new(level).map_err(|e| {
397
316
  RubyErr::new(
@@ -399,7 +318,7 @@ fn frame_codec_new_impl(class: VALUE, rb_dict: VALUE, id: VALUE, level: VALUE) -
399
318
  format!("CompressContext::new failed: {e}"),
400
319
  )
401
320
  })?;
402
- (0, None, None, cctx, DecompressContext::new())
321
+ (0, None, cctx, DecompressContext::new())
403
322
  }
404
323
  Some(bytes) => {
405
324
  let dict = load_dict(&bytes)?;
@@ -411,36 +330,42 @@ fn frame_codec_new_impl(class: VALUE, rb_dict: VALUE, id: VALUE, level: VALUE) -
411
330
  )
412
331
  })?;
413
332
  let dctx = DecompressContext::with_dict(dict.clone());
414
- (dl, Some(id), Some(dict), cctx, dctx)
333
+ (dl, Some(id), cctx, dctx)
415
334
  }
416
335
  };
417
336
 
418
- unsafe {
337
+ let codec = unsafe {
419
338
  rb::wrap_typed_data(
420
339
  class,
421
340
  Box::new(FrameCodec {
422
341
  dict_len,
423
342
  dict_id,
424
- dict,
425
343
  level,
426
344
  cctx: Mutex::new(cctx),
427
345
  dctx: Mutex::new(dctx),
428
346
  }),
429
347
  frame_codec_data_type(),
430
- )
431
- }
348
+ )?
349
+ };
350
+ rb::make_shareable(codec)?;
351
+ Ok(codec)
432
352
  }
433
353
 
434
354
  fn frame_codec_compress_impl(rb_self: VALUE, rb_input: VALUE) -> RbResult<VALUE> {
435
355
  let rb_self = unsafe { frame_codec_ref(rb_self)? };
436
356
  let mut input = rb::input_bytes(rb_input)?;
437
357
  let release_gvl = should_release_compress_gvl(input.len());
438
- input.lock_for_without_gvl(release_gvl)?;
439
- let out = with_mutex(&rb_self.cctx, release_gvl, "FrameCodec CCtx", |cctx| {
440
- cctx.compress(input.as_slice())
441
- .map(|out| out.into_owned())
442
- .map_err(|e| RubyErr::new(compress_error(), format!("zstd compress failed: {e}")))
443
- })?;
358
+ let out = with_mutex(
359
+ &rb_self.cctx,
360
+ release_gvl,
361
+ "FrameCodec CCtx",
362
+ &mut input,
363
+ |cctx, input| {
364
+ cctx.compress(input)
365
+ .map(|out| out.into_owned())
366
+ .map_err(|e| RubyErr::new(compress_error(), format!("zstd compress failed: {e}")))
367
+ },
368
+ )?;
444
369
  rb::new_binary_string(&out)
445
370
  }
446
371
 
@@ -450,18 +375,22 @@ fn frame_codec_decompress_impl(
450
375
  max_output: VALUE,
451
376
  ) -> RbResult<VALUE> {
452
377
  let rb_self = unsafe { frame_codec_ref(rb_self)? };
453
- let mut compressed = rb::input_bytes(rb_input)?;
454
378
  let max_output = rb::value_to_usize(max_output)?;
379
+ let mut compressed = rb::input_bytes(rb_input)?;
455
380
  let release_gvl = should_release_frame_decompress_gvl(decompress_work_size(
456
381
  compressed.as_slice(),
457
382
  max_output,
458
383
  ));
459
- compressed.lock_for_without_gvl(release_gvl)?;
460
- let dict = rb_self.dict.clone();
461
- let out = with_mutex(&rb_self.dctx, release_gvl, "FrameCodec DCtx", |dctx| {
462
- decompress_bounded(compressed.as_slice(), max_output, dctx, dict.as_ref())
463
- .map_err(|e| bounded_err(e, "zstd frame decode failed"))
464
- })?;
384
+ let out = with_mutex(
385
+ &rb_self.dctx,
386
+ release_gvl,
387
+ "FrameCodec DCtx",
388
+ &mut compressed,
389
+ |dctx, compressed| {
390
+ decompress_bounded(compressed, max_output, dctx)
391
+ .map_err(|e| bounded_err(e, "zstd frame decode failed"))
392
+ },
393
+ )?;
465
394
  rb::new_binary_string(&out)
466
395
  }
467
396
 
@@ -543,7 +472,6 @@ unsafe extern "C" fn frame_codec_get_frame_content_size(_class: VALUE, rb_input:
543
472
  struct BlockCodec {
544
473
  dict_len: usize,
545
474
  dict_id: Option<u32>,
546
- dict: Option<Dictionary>,
547
475
  level: i32,
548
476
  cctx: Mutex<CompressContext>,
549
477
  dctx: Mutex<DecompressContext>,
@@ -553,7 +481,7 @@ fn block_codec_new_impl(class: VALUE, rb_dict: VALUE, id: VALUE, level: VALUE) -
553
481
  let rb_dict = rb::value_to_option_bytes(rb_dict)?;
554
482
  let id = rb::value_to_u32(id)?;
555
483
  let level = rb::value_to_i32(level)?;
556
- let (dict_len, dict_id, dict, cctx, dctx) = match rb_dict {
484
+ let (dict_len, dict_id, cctx, dctx) = match rb_dict {
557
485
  None => {
558
486
  let cctx = CompressContext::new(level).map_err(|e| {
559
487
  RubyErr::new(
@@ -561,7 +489,7 @@ fn block_codec_new_impl(class: VALUE, rb_dict: VALUE, id: VALUE, level: VALUE) -
561
489
  format!("CompressContext::new failed: {e}"),
562
490
  )
563
491
  })?;
564
- (0, None, None, cctx, DecompressContext::new())
492
+ (0, None, cctx, DecompressContext::new())
565
493
  }
566
494
  Some(bytes) => {
567
495
  let dict = load_dict(&bytes)?;
@@ -573,7 +501,7 @@ fn block_codec_new_impl(class: VALUE, rb_dict: VALUE, id: VALUE, level: VALUE) -
573
501
  )
574
502
  })?;
575
503
  let dctx = DecompressContext::with_dict(dict.clone());
576
- (dl, Some(id), Some(dict), cctx, dctx)
504
+ (dl, Some(id), cctx, dctx)
577
505
  }
578
506
  };
579
507
 
@@ -583,7 +511,6 @@ fn block_codec_new_impl(class: VALUE, rb_dict: VALUE, id: VALUE, level: VALUE) -
583
511
  Box::new(BlockCodec {
584
512
  dict_len,
585
513
  dict_id,
586
- dict,
587
514
  level,
588
515
  cctx: Mutex::new(cctx),
589
516
  dctx: Mutex::new(dctx),
@@ -597,12 +524,17 @@ fn block_codec_compress_impl(rb_self: VALUE, rb_input: VALUE) -> RbResult<VALUE>
597
524
  let rb_self = unsafe { block_codec_ref(rb_self)? };
598
525
  let mut input = rb::input_bytes(rb_input)?;
599
526
  let release_gvl = should_release_compress_gvl(input.len());
600
- input.lock_for_without_gvl(release_gvl)?;
601
- let out = with_mutex(&rb_self.cctx, release_gvl, "BlockCodec CCtx", |cctx| {
602
- cctx.compress(input.as_slice())
603
- .map(|out| out.into_owned())
604
- .map_err(|e| RubyErr::new(compress_error(), format!("zstd compress failed: {e}")))
605
- })?;
527
+ let out = with_mutex(
528
+ &rb_self.cctx,
529
+ release_gvl,
530
+ "BlockCodec CCtx",
531
+ &mut input,
532
+ |cctx, input| {
533
+ cctx.compress(input)
534
+ .map(|out| out.into_owned())
535
+ .map_err(|e| RubyErr::new(compress_error(), format!("zstd compress failed: {e}")))
536
+ },
537
+ )?;
606
538
  rb::new_binary_string(&out)
607
539
  }
608
540
 
@@ -612,17 +544,18 @@ fn block_codec_decompress_impl(
612
544
  max_output: VALUE,
613
545
  ) -> RbResult<VALUE> {
614
546
  let rb_self = unsafe { block_codec_ref(rb_self)? };
615
- let compressed = rb::input_bytes(rb_input)?;
616
547
  let max_output = rb::value_to_usize(max_output)?;
617
- let out = with_mutex(&rb_self.dctx, false, "BlockCodec DCtx", |dctx| {
618
- decompress_bounded(
619
- compressed.as_slice(),
620
- max_output,
621
- dctx,
622
- rb_self.dict.as_ref(),
623
- )
624
- .map_err(|e| bounded_err(e, "zstd block decode failed"))
625
- })?;
548
+ let mut compressed = rb::input_bytes(rb_input)?;
549
+ let out = with_mutex(
550
+ &rb_self.dctx,
551
+ false,
552
+ "BlockCodec DCtx",
553
+ &mut compressed,
554
+ |dctx, compressed| {
555
+ decompress_bounded(compressed, max_output, dctx)
556
+ .map_err(|e| bounded_err(e, "zstd block decode failed"))
557
+ },
558
+ )?;
626
559
  rb::new_binary_string(&out)
627
560
  }
628
561
 
@@ -704,6 +637,7 @@ fn dict_trainer_new_impl(class: VALUE, max_dict_size: VALUE) -> RbResult<VALUE>
704
637
  }
705
638
 
706
639
  fn dict_trainer_add_sample_impl(rb_self: VALUE, rb_data: VALUE) -> RbResult<VALUE> {
640
+ let data = rb::value_to_bytes(rb_data)?;
707
641
  let rb_self = unsafe { dict_trainer_ref(rb_self)? };
708
642
  let mut borrow = rb_self
709
643
  .inner
@@ -712,7 +646,6 @@ fn dict_trainer_add_sample_impl(rb_self: VALUE, rb_data: VALUE) -> RbResult<VALU
712
646
  let state = borrow
713
647
  .as_mut()
714
648
  .ok_or_else(|| RubyErr::runtime("DictTrainer already consumed by #train"))?;
715
- let data = rb::value_to_bytes(rb_data)?;
716
649
  if data.len() < 4 {
717
650
  return Ok(rb::qnil());
718
651
  }
@@ -960,11 +893,11 @@ mod tests {
960
893
  compressed.extend_from_slice(&zstd::compress(&[b'b'; 100], 1).unwrap());
961
894
 
962
895
  assert!(matches!(
963
- decompress_bounded(&compressed, 199, &mut dctx, None),
896
+ decompress_bounded(&compressed, 199, &mut dctx),
964
897
  Err(BoundedError::OutputSizeLimit { .. })
965
898
  ));
966
899
  assert_eq!(
967
- decompress_bounded(&compressed, 200, &mut dctx, None)
900
+ decompress_bounded(&compressed, 200, &mut dctx)
968
901
  .unwrap()
969
902
  .len(),
970
903
  200
data/ext/zrip/src/rb.rs CHANGED
@@ -1,6 +1,4 @@
1
1
  use std::ffi::{c_char, c_long, c_void, CStr, CString};
2
- #[cfg(ruby_engine = "mri")]
3
- use std::mem::MaybeUninit;
4
2
  use std::panic::{catch_unwind, AssertUnwindSafe};
5
3
  use std::ptr;
6
4
 
@@ -32,7 +30,7 @@ unsafe extern "C" {
32
30
 
33
31
  #[derive(Debug)]
34
32
  pub enum RubyErr {
35
- Exception(VALUE),
33
+ Jump(i32),
36
34
  Error { class: VALUE, message: String },
37
35
  }
38
36
 
@@ -55,15 +53,6 @@ impl RubyErr {
55
53
  pub fn type_error(message: impl Into<String>) -> Self {
56
54
  Self::new(unsafe { rb_sys::rb_eTypeError }, message)
57
55
  }
58
-
59
- fn current_exception() -> Self {
60
- let err = unsafe { rb_sys::rb_errinfo() };
61
- if err == qnil() {
62
- Self::runtime("Ruby exception")
63
- } else {
64
- Self::Exception(err)
65
- }
66
- }
67
56
  }
68
57
 
69
58
  pub fn wrap<F>(f: F) -> VALUE
@@ -89,12 +78,15 @@ where
89
78
  }
90
79
 
91
80
  pub fn raise(err: RubyErr) -> ! {
92
- let exc = match err {
93
- RubyErr::Exception(exc) => exc,
94
- RubyErr::Error { class, message } => error_exception(class, message),
95
- };
96
-
97
- unsafe { rb_sys::rb_exc_raise(exc) }
81
+ match err {
82
+ // Preserve Ruby's tag and pending error info for Thread#kill and
83
+ // catch/throw, as well as ordinary exceptions.
84
+ RubyErr::Jump(state) => unsafe { rb_sys::rb_jump_tag(state) },
85
+ RubyErr::Error { class, message } => {
86
+ let exc = error_exception(class, message);
87
+ unsafe { rb_sys::rb_exc_raise(exc) }
88
+ }
89
+ }
98
90
  }
99
91
 
100
92
  fn error_exception(class: VALUE, message: String) -> VALUE {
@@ -145,7 +137,7 @@ where
145
137
  };
146
138
 
147
139
  if state != 0 {
148
- Err(RubyErr::current_exception())
140
+ Err(RubyErr::Jump(state))
149
141
  } else if data.panicked {
150
142
  Err(RubyErr::runtime("native Rust panic"))
151
143
  } else {
@@ -167,7 +159,7 @@ where
167
159
  #[cfg(ruby_engine = "mri")]
168
160
  struct WithoutGvlData<F, R> {
169
161
  func: Option<F>,
170
- output: MaybeUninit<R>,
162
+ output: Option<std::thread::Result<R>>,
171
163
  }
172
164
 
173
165
  #[cfg(ruby_engine = "mri")]
@@ -176,49 +168,55 @@ where
176
168
  F: FnOnce() -> R,
177
169
  {
178
170
  let data = unsafe { &mut *(data.cast::<WithoutGvlData<F, R>>()) };
179
- let func = data.func.take().expect("missing without-GVL function");
180
- data.output.write(func());
171
+ // Rust panics must be caught before returning through the C callback.
172
+ data.output = Some(catch_unwind(AssertUnwindSafe(|| {
173
+ data.func.take().expect("missing without-GVL function")()
174
+ })));
181
175
  ptr::null_mut()
182
176
  }
183
177
 
184
178
  #[cfg(ruby_engine = "mri")]
185
- fn without_gvl<F, R>(func: F) -> R
179
+ fn without_gvl<F, R>(func: F) -> RbResult<R>
186
180
  where
187
181
  F: FnOnce() -> R,
188
182
  {
189
183
  let mut data = WithoutGvlData {
190
184
  func: Some(func),
191
- output: MaybeUninit::uninit(),
185
+ output: None,
192
186
  };
193
- unsafe {
187
+ // Ruby checks interrupts before and after the callback. Protect the call
188
+ // so an exception still drops native output and unlocks borrowed strings.
189
+ protect_unit(|| unsafe {
194
190
  rb_thread_call_without_gvl(
195
191
  Some(without_gvl_trampoline::<F, R>),
196
192
  (&mut data as *mut WithoutGvlData<F, R>).cast::<c_void>(),
197
193
  None,
198
194
  ptr::null_mut(),
199
195
  );
200
- data.output.assume_init()
201
- }
196
+ })?;
197
+ data.output
198
+ .ok_or_else(|| RubyErr::runtime("without-GVL callback did not run"))?
199
+ .map_err(|_| RubyErr::runtime("native Rust panic"))
202
200
  }
203
201
 
204
202
  #[cfg(ruby_engine = "mri")]
205
- pub fn maybe_without_gvl<F, R>(release_gvl: bool, func: F) -> R
203
+ pub fn maybe_without_gvl<F, R>(release_gvl: bool, func: F) -> RbResult<R>
206
204
  where
207
205
  F: FnOnce() -> R,
208
206
  {
209
207
  if release_gvl {
210
208
  without_gvl(func)
211
209
  } else {
212
- func()
210
+ Ok(func())
213
211
  }
214
212
  }
215
213
 
216
214
  #[cfg(not(ruby_engine = "mri"))]
217
- pub fn maybe_without_gvl<F, R>(_release_gvl: bool, func: F) -> R
215
+ pub fn maybe_without_gvl<F, R>(_release_gvl: bool, func: F) -> RbResult<R>
218
216
  where
219
217
  F: FnOnce() -> R,
220
218
  {
221
- func()
219
+ Ok(func())
222
220
  }
223
221
 
224
222
  pub const fn qnil() -> VALUE {
@@ -255,8 +253,23 @@ pub fn string_value(value: VALUE) -> RbResult<VALUE> {
255
253
  protect_value(|| unsafe { rb_sys::rb_str_to_str(value) })
256
254
  }
257
255
 
258
- pub fn freeze_value(value: VALUE) -> RbResult<()> {
259
- protect_unit(|| unsafe { rb_sys::RB_OBJ_FREEZE(value) })
256
+ pub fn frozen_shareable_flags() -> VALUE {
257
+ #[cfg(ruby_engine = "mri")]
258
+ {
259
+ // RUBY_TYPED_FROZEN_SHAREABLE equals RUBY_FL_SHAREABLE in Ruby headers.
260
+ 1 | rb_sys::ruby_fl_type::RUBY_FL_SHAREABLE as VALUE
261
+ }
262
+ #[cfg(not(ruby_engine = "mri"))]
263
+ {
264
+ 1
265
+ }
266
+ }
267
+
268
+ pub fn make_shareable(value: VALUE) -> RbResult<()> {
269
+ protect_unit(|| unsafe { rb_sys::RB_OBJ_FREEZE(value) })?;
270
+ #[cfg(ruby_engine = "mri")]
271
+ protect_value(|| unsafe { rb_sys::rb_ractor_make_shareable(value) })?;
272
+ Ok(())
260
273
  }
261
274
 
262
275
  pub fn value_to_bytes(value: VALUE) -> RbResult<Vec<u8>> {
@@ -613,3 +626,42 @@ unsafe fn transmute_3(
613
626
  fn c_long_len(len: usize) -> RbResult<c_long> {
614
627
  c_long::try_from(len).map_err(|_| RubyErr::arg("length too large"))
615
628
  }
629
+
630
+ #[cfg(all(test, ruby_engine = "mri"))]
631
+ mod tests {
632
+ use super::*;
633
+
634
+ fn run_callback<F, R>(func: F) -> std::thread::Result<R>
635
+ where
636
+ F: FnOnce() -> R,
637
+ {
638
+ let mut data = WithoutGvlData {
639
+ func: Some(func),
640
+ output: None,
641
+ };
642
+ unsafe {
643
+ without_gvl_trampoline::<F, R>(
644
+ (&mut data as *mut WithoutGvlData<F, R>).cast::<c_void>(),
645
+ );
646
+ }
647
+ data.output.unwrap()
648
+ }
649
+
650
+ #[test]
651
+ fn callback_catches_panics_before_returning_to_c() {
652
+ let dropped = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
653
+ struct OnDrop(std::sync::Arc<std::sync::atomic::AtomicBool>);
654
+ impl Drop for OnDrop {
655
+ fn drop(&mut self) {
656
+ self.0.store(true, std::sync::atomic::Ordering::SeqCst);
657
+ }
658
+ }
659
+ let captured = OnDrop(dropped.clone());
660
+ let result = run_callback(move || {
661
+ let _captured = captured;
662
+ panic!("callback panic");
663
+ });
664
+ assert!(result.is_err());
665
+ assert!(dropped.load(std::sync::atomic::Ordering::SeqCst));
666
+ }
667
+ }
data/lib/zrip/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Zrip
4
- VERSION = "0.2.0"
4
+ VERSION = "0.2.1"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zrip
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrik Wenger
@@ -16,6 +16,9 @@ dependencies:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
18
  version: '0.9'
19
+ - - ">="
20
+ - !ruby/object:Gem::Version
21
+ version: 0.9.130
19
22
  type: :runtime
20
23
  prerelease: false
21
24
  version_requirements: !ruby/object:Gem::Requirement
@@ -23,6 +26,9 @@ dependencies:
23
26
  - - "~>"
24
27
  - !ruby/object:Gem::Version
25
28
  version: '0.9'
29
+ - - ">="
30
+ - !ruby/object:Gem::Version
31
+ version: 0.9.130
26
32
  description: Ruby bindings (via Rust/rb-sys) for zrip, a pure-Rust Zstandard implementation.
27
33
  Frame-format and block-format compress/decompress with optional dictionary support,
28
34
  configurable compression levels, and FastCOVER-based dictionary training. Ractor-safe.
@@ -54,7 +60,7 @@ licenses:
54
60
  - MIT
55
61
  metadata:
56
62
  homepage_uri: https://github.com/paddor/zrip-rb
57
- source_code_uri: https://github.com/paddor/zrip-rb
63
+ source_code_uri: https://github.com/paddor/zrip-rb/tree/main
58
64
  documentation_uri: https://rubydoc.info/gems/zrip
59
65
  changelog_uri: https://github.com/paddor/zrip-rb/blob/main/CHANGELOG.md
60
66
  rubygems_mfa_required: 'true'
@@ -72,7 +78,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
72
78
  - !ruby/object:Gem::Version
73
79
  version: '0'
74
80
  requirements: []
75
- rubygems_version: 4.0.16
81
+ rubygems_version: 4.0.20
76
82
  specification_version: 4
77
83
  summary: Ractor-safe Zstandard bindings for Ruby (pure-Rust zrip backend)
78
84
  test_files: []