ciborium 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.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +9 -0
  3. data/LICENSE.txt +21 -0
  4. data/NOTICE.md +26 -0
  5. data/README.md +221 -0
  6. data/Rakefile +46 -0
  7. data/ext/ciborium/Cargo.lock +339 -0
  8. data/ext/ciborium/Cargo.toml +26 -0
  9. data/ext/ciborium/build.rs +4 -0
  10. data/ext/ciborium/extconf.rb +8 -0
  11. data/ext/ciborium/src/canonical.rs +60 -0
  12. data/ext/ciborium/src/cursor.rs +106 -0
  13. data/ext/ciborium/src/decode.rs +325 -0
  14. data/ext/ciborium/src/encode.rs +330 -0
  15. data/ext/ciborium/src/error.rs +53 -0
  16. data/ext/ciborium/src/lib.rs +153 -0
  17. data/ext/ciborium/src/scan.rs +223 -0
  18. data/lib/ciborium/compat/buffer.rb +190 -0
  19. data/lib/ciborium/compat/errors.rb +30 -0
  20. data/lib/ciborium/compat/packer.rb +90 -0
  21. data/lib/ciborium/compat/unpacker.rb +111 -0
  22. data/lib/ciborium/compat.rb +110 -0
  23. data/lib/ciborium/decode_options.rb +61 -0
  24. data/lib/ciborium/decoder.rb +141 -0
  25. data/lib/ciborium/encode_options.rb +41 -0
  26. data/lib/ciborium/encoded.rb +10 -0
  27. data/lib/ciborium/encoder.rb +80 -0
  28. data/lib/ciborium/errors.rb +25 -0
  29. data/lib/ciborium/extensions/bignum.rb +35 -0
  30. data/lib/ciborium/extensions/regexp.rb +41 -0
  31. data/lib/ciborium/extensions/time.rb +39 -0
  32. data/lib/ciborium/extensions/uri.rb +29 -0
  33. data/lib/ciborium/extensions.rb +92 -0
  34. data/lib/ciborium/native.rb +25 -0
  35. data/lib/ciborium/replace.rb +105 -0
  36. data/lib/ciborium/simple.rb +21 -0
  37. data/lib/ciborium/tagged.rb +18 -0
  38. data/lib/ciborium/version.rb +6 -0
  39. data/lib/ciborium.rb +65 -0
  40. data/sig/generated/ciborium/compat/buffer.rbs +95 -0
  41. data/sig/generated/ciborium/compat/errors.rbs +23 -0
  42. data/sig/generated/ciborium/compat/packer.rbs +53 -0
  43. data/sig/generated/ciborium/compat/unpacker.rbs +59 -0
  44. data/sig/generated/ciborium/compat.rbs +57 -0
  45. data/sig/generated/ciborium/decode_options.rbs +41 -0
  46. data/sig/generated/ciborium/decoder.rbs +85 -0
  47. data/sig/generated/ciborium/encode_options.rbs +33 -0
  48. data/sig/generated/ciborium/encoded.rbs +16 -0
  49. data/sig/generated/ciborium/encoder.rbs +50 -0
  50. data/sig/generated/ciborium/errors.rbs +32 -0
  51. data/sig/generated/ciborium/extensions/bignum.rbs +20 -0
  52. data/sig/generated/ciborium/extensions/regexp.rbs +30 -0
  53. data/sig/generated/ciborium/extensions/time.rbs +22 -0
  54. data/sig/generated/ciborium/extensions/uri.rbs +20 -0
  55. data/sig/generated/ciborium/extensions.rbs +35 -0
  56. data/sig/generated/ciborium/native.rbs +10 -0
  57. data/sig/generated/ciborium/replace.rbs +37 -0
  58. data/sig/generated/ciborium/version.rbs +5 -0
  59. data/sig/generated/ciborium.rbs +30 -0
  60. data/sig/manual/ciborium/extensions.rbs +12 -0
  61. data/sig/manual/ciborium/native.rbs +23 -0
  62. data/sig/manual/ciborium/tagged.rbs +25 -0
  63. metadata +123 -0
@@ -0,0 +1,26 @@
1
+ [package]
2
+ name = "ciborium_native"
3
+ version = "0.1.0"
4
+ edition = "2024"
5
+ publish = false
6
+ description = "Ruby native extension for the ciborium gem"
7
+ license = "MIT"
8
+
9
+ [lib]
10
+ crate-type = ["cdylib"]
11
+
12
+ [dependencies]
13
+ magnus = { version = "0.9", features = ["rb-sys"] }
14
+ rb-sys = { version = "0.9.130", default-features = false, features = ["stable-api-compiled-fallback"] }
15
+ ciborium-ll = { version = "0.2.2", features = ["std"] }
16
+ ciborium-io = { version = "0.2.2", features = ["std"] }
17
+ half = "2.7"
18
+
19
+ [build-dependencies]
20
+ rb-sys-env = "0.2"
21
+
22
+ [profile.release]
23
+ debug = 0
24
+ strip = true
25
+ lto = true
26
+ codegen-units = 1
@@ -0,0 +1,4 @@
1
+ fn main() -> Result<(), Box<dyn std::error::Error>> {
2
+ let _ = rb_sys_env::activate()?;
3
+ Ok(())
4
+ }
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mkmf"
4
+ require "rb_sys/mkmf"
5
+
6
+ create_rust_makefile("ciborium/ciborium_native") do |r|
7
+ r.profile = ENV.fetch("RB_SYS_CARGO_PROFILE", :release).to_sym
8
+ end
@@ -0,0 +1,60 @@
1
+ /// Rules of the CTAP2 canonical CBOR encoding form (CTAP 2.2 section 8), which WebAuthn
2
+ /// requires of every CBOR structure it defines.
3
+ pub(crate) struct Ctap2;
4
+
5
+ impl Ctap2 {
6
+ /// Checks the head whose initial byte is `initial` and which parsed as `header`.
7
+ pub(crate) fn check_head(
8
+ initial: u8,
9
+ header: &ciborium_ll::Header,
10
+ start: usize,
11
+ ) -> Result<(), crate::error::Error> {
12
+ let argument = match *header {
13
+ ciborium_ll::Header::Tag(_) => {
14
+ return Err(crate::error::Error::malformed(format!(
15
+ "tag at offset {start} is not allowed by the ctap2 profile"
16
+ )));
17
+ }
18
+ ciborium_ll::Header::Bytes(None)
19
+ | ciborium_ll::Header::Text(None)
20
+ | ciborium_ll::Header::Array(None)
21
+ | ciborium_ll::Header::Map(None)
22
+ | ciborium_ll::Header::Break => {
23
+ return Err(crate::error::Error::malformed(format!(
24
+ "indefinite-length item at offset {start} is not allowed by the ctap2 profile"
25
+ )));
26
+ }
27
+ ciborium_ll::Header::Positive(n) | ciborium_ll::Header::Negative(n) => n,
28
+ ciborium_ll::Header::Bytes(Some(len))
29
+ | ciborium_ll::Header::Text(Some(len))
30
+ | ciborium_ll::Header::Array(Some(len))
31
+ | ciborium_ll::Header::Map(Some(len)) => len as u64,
32
+ // Floats keep whatever width they were encoded with.
33
+ ciborium_ll::Header::Float(_) | ciborium_ll::Header::Simple(_) => return Ok(()),
34
+ };
35
+ if initial & 0x1f != crate::canonical::Ctap2::shortest_additional_info(argument) {
36
+ return Err(crate::error::Error::malformed(format!(
37
+ "argument at offset {start} is not in its shortest form"
38
+ )));
39
+ }
40
+ Ok(())
41
+ }
42
+
43
+ fn shortest_additional_info(argument: u64) -> u8 {
44
+ match argument {
45
+ 0..=23 => argument as u8,
46
+ 24..=0xff => 24,
47
+ 0x100..=0xffff => 25,
48
+ 0x1_0000..=0xffff_ffff => 26,
49
+ _ => 27,
50
+ }
51
+ }
52
+
53
+ /// Orders encoded map keys: by major type, then by length, then bytewise.
54
+ pub(crate) fn key_order(a: &[u8], b: &[u8]) -> std::cmp::Ordering {
55
+ (a[0] >> 5)
56
+ .cmp(&(b[0] >> 5))
57
+ .then(a.len().cmp(&b.len()))
58
+ .then(a.cmp(b))
59
+ }
60
+ }
@@ -0,0 +1,106 @@
1
+ /// A read position over an input buffer, parsing item heads with ciborium-ll.
2
+ pub(crate) struct Cursor<'a> {
3
+ input: &'a [u8],
4
+ pos: usize,
5
+ }
6
+
7
+ impl<'a> Cursor<'a> {
8
+ pub(crate) fn new(input: &'a [u8], pos: usize) -> Self {
9
+ Self { input, pos }
10
+ }
11
+
12
+ pub(crate) fn pos(&self) -> usize {
13
+ self.pos
14
+ }
15
+
16
+ pub(crate) fn input(&self) -> &'a [u8] {
17
+ self.input
18
+ }
19
+
20
+ pub(crate) fn remaining(&self) -> usize {
21
+ self.input.len().saturating_sub(self.pos)
22
+ }
23
+
24
+ /// Reads the next item head. ciborium-ll normalises the argument, so checks that depend on
25
+ /// the encoded form look at the raw bytes instead.
26
+ pub(crate) fn head(&mut self) -> Result<ciborium_ll::Header, crate::error::Error> {
27
+ let start = self.pos;
28
+ let rest = self.input.get(start..).unwrap_or_default();
29
+ let mut decoder = ciborium_ll::Decoder::from(rest);
30
+ let header = decoder.pull().map_err(|error| match error {
31
+ ciborium_ll::Error::Io(_) => crate::error::Error::Incomplete,
32
+ ciborium_ll::Error::Syntax(_) => crate::error::Error::malformed(format!(
33
+ "invalid initial byte 0x{:02x} at offset {start}",
34
+ rest[0]
35
+ )),
36
+ })?;
37
+ // RFC 8949 section 3.3: two-byte simple values below 32 are not well-formed.
38
+ if rest[0] == 0xf8 && rest[1] < 32 {
39
+ return Err(crate::error::Error::malformed(format!(
40
+ "invalid simple value encoding at offset {start}"
41
+ )));
42
+ }
43
+ self.pos += decoder.offset();
44
+ Ok(header)
45
+ }
46
+
47
+ pub(crate) fn take(&mut self, len: usize) -> Result<&'a [u8], crate::error::Error> {
48
+ if len > self.remaining() {
49
+ return Err(crate::error::Error::Incomplete);
50
+ }
51
+ let bytes = &self.input[self.pos..self.pos + len];
52
+ self.pos += len;
53
+ Ok(bytes)
54
+ }
55
+
56
+ /// Reads the body of a byte or text string whose head declared `len`, joining the chunks of
57
+ /// an indefinite-length string. Text is checked to be UTF-8.
58
+ pub(crate) fn string(
59
+ &mut self,
60
+ len: Option<usize>,
61
+ text: bool,
62
+ ) -> Result<std::borrow::Cow<'a, [u8]>, crate::error::Error> {
63
+ let Some(len) = len else {
64
+ return self.indefinite_string(text).map(std::borrow::Cow::Owned);
65
+ };
66
+ let start = self.pos;
67
+ let bytes = self.take(len)?;
68
+ if text {
69
+ crate::cursor::Cursor::check_utf8(bytes, start)?;
70
+ }
71
+ Ok(std::borrow::Cow::Borrowed(bytes))
72
+ }
73
+
74
+ fn indefinite_string(&mut self, text: bool) -> Result<Vec<u8>, crate::error::Error> {
75
+ let mut buffer = Vec::new();
76
+ loop {
77
+ let start = self.pos;
78
+ let len = match self.head()? {
79
+ ciborium_ll::Header::Break => return Ok(buffer),
80
+ ciborium_ll::Header::Bytes(Some(len)) if !text => len,
81
+ ciborium_ll::Header::Text(Some(len)) if text => len,
82
+ _ => {
83
+ return Err(crate::error::Error::malformed(format!(
84
+ "invalid chunk in indefinite-length string at offset {start}"
85
+ )));
86
+ }
87
+ };
88
+ let chunk_start = self.pos;
89
+ let chunk = self.take(len)?;
90
+ // RFC 8949 section 3.2.3: each chunk of a text string is valid UTF-8 by itself.
91
+ if text {
92
+ crate::cursor::Cursor::check_utf8(chunk, chunk_start)?;
93
+ }
94
+ buffer.extend_from_slice(chunk);
95
+ }
96
+ }
97
+
98
+ fn check_utf8(bytes: &[u8], start: usize) -> Result<(), crate::error::Error> {
99
+ match std::str::from_utf8(bytes) {
100
+ Ok(_) => Ok(()),
101
+ Err(_) => Err(crate::error::Error::malformed(format!(
102
+ "invalid UTF-8 in text string at offset {start}"
103
+ ))),
104
+ }
105
+ }
106
+ }
@@ -0,0 +1,325 @@
1
+ pub(crate) const FLAG_SYMBOLIZE_KEYS: u32 = 1;
2
+ pub(crate) const FLAG_LAST_WINS: u32 = 1 << 1;
3
+ pub(crate) const FLAG_CTAP2: u32 = 1 << 2;
4
+
5
+ // Nested arrays all draw on the same remaining input, so the declared length alone must not size
6
+ // them.
7
+ const PREALLOCATE_LIMIT: usize = 256;
8
+
9
+ enum Frame {
10
+ Array {
11
+ array: magnus::RArray,
12
+ remaining: Option<usize>,
13
+ },
14
+ Map {
15
+ hash: magnus::RHash,
16
+ remaining: Option<usize>,
17
+ key: Option<magnus::Value>,
18
+ // Byte ranges of the current and the previous key, for the ctap2 profile's ordering.
19
+ key_start: usize,
20
+ previous_key: Option<(usize, usize)>,
21
+ },
22
+ Tag {
23
+ tag: u64,
24
+ },
25
+ }
26
+
27
+ /// Builds one data item into Ruby objects without recursion, so nesting depth is bounded by
28
+ /// `max_depth` rather than by the native stack.
29
+ pub(crate) struct Decoder<'a> {
30
+ ruby: &'a magnus::Ruby,
31
+ cursor: crate::cursor::Cursor<'a>,
32
+ max_depth: usize,
33
+ flags: u32,
34
+ // Tag number to a callable taking (tag, content), for allowlisted extensions only.
35
+ handlers: Option<magnus::RHash>,
36
+ stack: Vec<Frame>,
37
+ // Containers held in `stack` live on the Rust heap, which the GC does not scan. This array
38
+ // keeps them reachable, and is itself reachable through the Decoder on the native stack. It
39
+ // is not registered globally, so a NoMemoryError unwinding past Rust leaves nothing behind.
40
+ pin: magnus::RArray,
41
+ tagged_class: magnus::RClass,
42
+ simple_class: magnus::RClass,
43
+ }
44
+
45
+ impl<'a> Decoder<'a> {
46
+ pub(crate) fn new(
47
+ ruby: &'a magnus::Ruby,
48
+ input: &'a [u8],
49
+ pos: usize,
50
+ max_depth: usize,
51
+ flags: u32,
52
+ handlers: Option<magnus::RHash>,
53
+ ) -> Result<Self, crate::error::Error> {
54
+ use magnus::Module as _;
55
+ let ciborium = ruby
56
+ .class_object()
57
+ .const_get::<_, magnus::RModule>("Ciborium")?;
58
+ Ok(Self {
59
+ ruby,
60
+ cursor: crate::cursor::Cursor::new(input, pos),
61
+ max_depth,
62
+ flags,
63
+ handlers,
64
+ stack: Vec::new(),
65
+ pin: ruby.ary_new(),
66
+ tagged_class: ciborium.const_get("Tagged")?,
67
+ simple_class: ciborium.const_get("Simple")?,
68
+ })
69
+ }
70
+
71
+ pub(crate) fn pos(&self) -> usize {
72
+ self.cursor.pos()
73
+ }
74
+
75
+ /// The array keeping decoded containers reachable. Callers hold it live until decoding ends.
76
+ pub(crate) fn pin(&self) -> magnus::RArray {
77
+ self.pin
78
+ }
79
+
80
+ /// Decodes the item at the current position and advances past it.
81
+ pub(crate) fn decode(&mut self) -> Result<magnus::Value, crate::error::Error> {
82
+ use magnus::value::ReprValue as _;
83
+ loop {
84
+ let start = self.cursor.pos();
85
+ if let Some(Frame::Map {
86
+ key: None,
87
+ key_start,
88
+ ..
89
+ }) = self.stack.last_mut()
90
+ {
91
+ *key_start = start;
92
+ }
93
+ let header = self.cursor.head()?;
94
+ if self.flags & FLAG_CTAP2 != 0 {
95
+ crate::canonical::Ctap2::check_head(self.cursor.input()[start], &header, start)?;
96
+ }
97
+ let value = match header {
98
+ ciborium_ll::Header::Positive(n) => self.ruby.integer_from_u64(n).as_value(),
99
+ ciborium_ll::Header::Negative(n) => self.negative(n)?,
100
+ ciborium_ll::Header::Float(f) => self.ruby.float_from_f64(f).as_value(),
101
+ ciborium_ll::Header::Simple(n) => self.simple(n)?,
102
+ ciborium_ll::Header::Bytes(len) => self.string(len, false)?,
103
+ ciborium_ll::Header::Text(len) => self.string(len, true)?,
104
+ ciborium_ll::Header::Break => self.close_indefinite(start)?,
105
+ ciborium_ll::Header::Array(len) => {
106
+ if len == Some(0) {
107
+ self.ruby.ary_new().as_value()
108
+ } else {
109
+ let capacity = len
110
+ .map_or(0, |n| n.min(self.cursor.remaining()).min(PREALLOCATE_LIMIT));
111
+ let array = self.ruby.ary_new_capa(capacity);
112
+ self.open(Frame::Array {
113
+ array,
114
+ remaining: len,
115
+ })?;
116
+ continue;
117
+ }
118
+ }
119
+ ciborium_ll::Header::Map(len) => {
120
+ if len == Some(0) {
121
+ self.ruby.hash_new().as_value()
122
+ } else {
123
+ let hash = self.ruby.hash_new();
124
+ self.open(Frame::Map {
125
+ hash,
126
+ remaining: len,
127
+ key: None,
128
+ key_start: 0,
129
+ previous_key: None,
130
+ })?;
131
+ continue;
132
+ }
133
+ }
134
+ ciborium_ll::Header::Tag(tag) => {
135
+ self.open(Frame::Tag { tag })?;
136
+ continue;
137
+ }
138
+ };
139
+ if let Some(value) = self.complete(value)? {
140
+ return Ok(value);
141
+ }
142
+ }
143
+ }
144
+
145
+ fn open(&mut self, frame: Frame) -> Result<(), crate::error::Error> {
146
+ if self.stack.len() >= self.max_depth {
147
+ return Err(crate::error::Error::Depth(self.max_depth));
148
+ }
149
+ match &frame {
150
+ Frame::Array { array, .. } => self.pin.push(*array)?,
151
+ Frame::Map { hash, .. } => self.pin.push(*hash)?,
152
+ Frame::Tag { .. } => {}
153
+ }
154
+ self.stack.push(frame);
155
+ Ok(())
156
+ }
157
+
158
+ fn close(&mut self) -> Result<(), crate::error::Error> {
159
+ if let Some(Frame::Array { .. } | Frame::Map { .. }) = self.stack.pop() {
160
+ self.pin.pop::<magnus::Value>()?;
161
+ }
162
+ Ok(())
163
+ }
164
+
165
+ /// Hands a finished item to its enclosing container, finishing containers in turn. Returns
166
+ /// the top-level item once there is no container left.
167
+ fn complete(
168
+ &mut self,
169
+ mut value: magnus::Value,
170
+ ) -> Result<Option<magnus::Value>, crate::error::Error> {
171
+ use magnus::value::ReprValue as _;
172
+ loop {
173
+ let finished = match self.stack.last_mut() {
174
+ None => return Ok(Some(value)),
175
+ Some(Frame::Array { array, remaining }) => {
176
+ array.push(value)?;
177
+ match remaining {
178
+ Some(n) => {
179
+ *n -= 1;
180
+ if *n > 0 {
181
+ return Ok(None);
182
+ }
183
+ array.as_value()
184
+ }
185
+ None => return Ok(None),
186
+ }
187
+ }
188
+ Some(Frame::Map {
189
+ hash,
190
+ remaining,
191
+ key,
192
+ key_start,
193
+ previous_key,
194
+ }) => {
195
+ let Some(k) = key.take() else {
196
+ if self.flags & FLAG_CTAP2 != 0 {
197
+ let current = (*key_start, self.cursor.pos());
198
+ let input = self.cursor.input();
199
+ if let Some((from, to)) = *previous_key {
200
+ let order = crate::canonical::Ctap2::key_order(
201
+ &input[from..to],
202
+ &input[current.0..current.1],
203
+ );
204
+ if order != std::cmp::Ordering::Less {
205
+ return Err(crate::error::Error::malformed(format!(
206
+ "map key at offset {} is not in canonical order",
207
+ current.0
208
+ )));
209
+ }
210
+ }
211
+ *previous_key = Some(current);
212
+ }
213
+ *key = Some(value);
214
+ self.pin.push(value)?;
215
+ return Ok(None);
216
+ };
217
+ let size = hash.len();
218
+ hash.aset(k, value)?;
219
+ if hash.len() == size && self.flags & FLAG_LAST_WINS == 0 {
220
+ return Err(crate::error::Error::malformed("duplicate map key"));
221
+ }
222
+ let hash = *hash;
223
+ self.pin.pop::<magnus::Value>()?;
224
+ match remaining {
225
+ Some(n) => {
226
+ *n -= 1;
227
+ if *n > 0 {
228
+ return Ok(None);
229
+ }
230
+ hash.as_value()
231
+ }
232
+ None => return Ok(None),
233
+ }
234
+ }
235
+ Some(Frame::Tag { tag }) => {
236
+ let tag = *tag;
237
+ self.stack.pop();
238
+ value = self.tag(tag, value)?;
239
+ continue;
240
+ }
241
+ };
242
+ self.close()?;
243
+ value = finished;
244
+ }
245
+ }
246
+
247
+ fn close_indefinite(&mut self, start: usize) -> Result<magnus::Value, crate::error::Error> {
248
+ use magnus::value::ReprValue as _;
249
+ let value = match self.stack.last() {
250
+ Some(Frame::Array {
251
+ array,
252
+ remaining: None,
253
+ }) => array.as_value(),
254
+ Some(Frame::Map {
255
+ hash,
256
+ remaining: None,
257
+ key: None,
258
+ ..
259
+ }) => hash.as_value(),
260
+ _ => {
261
+ return Err(crate::error::Error::malformed(format!(
262
+ "unexpected break at offset {start}"
263
+ )));
264
+ }
265
+ };
266
+ self.close()?;
267
+ Ok(value)
268
+ }
269
+
270
+ fn negative(&self, n: u64) -> Result<magnus::Value, crate::error::Error> {
271
+ use magnus::value::ReprValue as _;
272
+ // The encoded argument n stands for -1 - n.
273
+ Ok(match i64::try_from(n) {
274
+ Ok(n) => self.ruby.integer_from_i64(-1 - n).as_value(),
275
+ Err(_) => self.ruby.integer_from_u64(n).funcall("~", ())?,
276
+ })
277
+ }
278
+
279
+ fn simple(&self, n: u8) -> Result<magnus::Value, crate::error::Error> {
280
+ use magnus::IntoValue as _;
281
+ use magnus::value::ReprValue as _;
282
+ Ok(match n {
283
+ 20 => false.into_value_with(self.ruby),
284
+ 21 => true.into_value_with(self.ruby),
285
+ 22 => self.ruby.qnil().as_value(),
286
+ // Data.new maps positional arguments to keywords; rb_class_new_instance would not.
287
+ n => self.simple_class.funcall("new", (n,))?,
288
+ })
289
+ }
290
+
291
+ fn tag(&self, tag: u64, value: magnus::Value) -> Result<magnus::Value, crate::error::Error> {
292
+ use magnus::value::ReprValue as _;
293
+ let tag = self.ruby.integer_from_u64(tag);
294
+ if let Some(handler) = self.handlers.and_then(|handlers| handlers.get(tag)) {
295
+ return Ok(handler.funcall("call", (tag, value))?);
296
+ }
297
+ Ok(self.tagged_class.funcall("new", (tag, value))?)
298
+ }
299
+
300
+ fn string(
301
+ &mut self,
302
+ len: Option<usize>,
303
+ text: bool,
304
+ ) -> Result<magnus::Value, crate::error::Error> {
305
+ use magnus::value::ReprValue as _;
306
+ let bytes = self.cursor.string(len, text)?;
307
+ if !text {
308
+ return Ok(self.ruby.str_from_slice(&bytes).as_value());
309
+ }
310
+ let string = self.ruby.enc_str_new(&bytes, self.ruby.utf8_encoding());
311
+ if self.flags & FLAG_SYMBOLIZE_KEYS != 0 && self.expects_key() {
312
+ // rb_str_intern creates a dynamic Symbol, which the GC can collect, unlike rb_intern.
313
+ // SAFETY: string is a live String created above, and the GVL is held.
314
+ let symbol =
315
+ unsafe { rb_sys::rb_str_intern(magnus::rb_sys::AsRawValue::as_raw(string)) };
316
+ // SAFETY: rb_str_intern returns a valid Symbol VALUE.
317
+ return Ok(unsafe { magnus::rb_sys::FromRawValue::from_raw(symbol) });
318
+ }
319
+ Ok(string.as_value())
320
+ }
321
+
322
+ fn expects_key(&self) -> bool {
323
+ matches!(self.stack.last(), Some(Frame::Map { key: None, .. }))
324
+ }
325
+ }