artichoke_backend/extn/core/string/
trampoline.rs

1use core::convert::TryFrom;
2use core::fmt::Write as _;
3use core::hash::BuildHasher;
4use core::str;
5
6use artichoke_core::hash::Hash as _;
7use artichoke_core::value::Value as _;
8use bstr::ByteSlice;
9
10use super::Encoding;
11use crate::convert::implicitly_convert_to_int;
12use crate::convert::implicitly_convert_to_nilable_string;
13use crate::convert::implicitly_convert_to_spinoso_string;
14use crate::convert::implicitly_convert_to_string;
15use crate::extn::core::array::Array;
16#[cfg(feature = "core-regexp")]
17use crate::extn::core::matchdata::{self, MatchData};
18#[cfg(feature = "core-regexp")]
19use crate::extn::core::regexp::{self, Regexp};
20use crate::extn::core::symbol::Symbol;
21use crate::extn::prelude::*;
22use crate::sys::protect;
23
24pub fn mul(interp: &mut Artichoke, mut value: Value, count: Value) -> Result<Value, Error> {
25    let count = implicitly_convert_to_int(interp, count)?;
26    let count = usize::try_from(count).map_err(|_| ArgumentError::with_message("negative argument"))?;
27
28    let s = unsafe { super::String::unbox_from_value(&mut value, interp)? };
29    // This guard ensures `repeat` below does not panic on `usize` overflow.
30    if count.checked_mul(s.len()).is_none() {
31        return Err(RangeError::with_message("bignum too big to convert into `long'").into());
32    }
33    let repeated_s = s.as_slice().repeat(count).into();
34    super::String::alloc_value(repeated_s, interp)
35}
36
37pub fn add(interp: &mut Artichoke, mut value: Value, mut other: Value) -> Result<Value, Error> {
38    let s = unsafe { super::String::unbox_from_value(&mut value, interp)? };
39    // SAFETY: The borrowed byte slice is immediately copied into the `s` byte
40    // buffer. There are no intervening interpreter accesses.
41    let to_append = unsafe { implicitly_convert_to_string(interp, &mut other)? };
42
43    let mut concatenated = s.clone();
44    // XXX: This call doesn't do a check to see if we'll exceed the max allocation
45    //    size and may panic or abort.
46    concatenated.extend_from_slice(to_append);
47    super::String::alloc_value(concatenated, interp)
48}
49
50pub fn append(interp: &mut Artichoke, mut value: Value, mut other: Value) -> Result<Value, Error> {
51    check_frozen(interp, value)?;
52
53    let mut s = unsafe { super::String::unbox_from_value(&mut value, interp)? };
54    if let Ok(int) = other.try_convert_into::<i64>(interp) {
55        // SAFETY: The string is repacked before any intervening uses of
56        // `interp` which means no mruby heap allocations can occur.
57        unsafe {
58            let string_mut = s.as_inner_mut();
59            // XXX: This call doesn't do a check to see if we'll exceed the max allocation
60            //    size and may panic or abort.
61            string_mut
62                .try_push_int(int)
63                .map_err(|err| RangeError::from(err.message()))?;
64            let s = s.take();
65            return super::String::box_into_value(s, value, interp);
66        }
67    }
68    // SAFETY: The byte slice is immediately used and discarded after extraction.
69    // There are no intervening interpreter accesses.
70    let other = unsafe { implicitly_convert_to_spinoso_string(interp, &mut other)? };
71    match s.encoding() {
72        // ```
73        // [3.1.2] > s = ""
74        // => ""
75        // [3.1.2] > b = "abc".b
76        // => "abc"
77        // [3.1.2] > s << b
78        // => "abc"
79        // [3.1.2] > s.encoding
80        // => #<Encoding:UTF-8>
81        // [3.1.2] > b = "\xFF\xFE".b
82        // => "\xFF\xFE"
83        // [3.1.2] > s = ""
84        // => ""
85        // [3.1.2] > s << b
86        // => "\xFF\xFE"
87        // [3.1.2] > s.encoding
88        // => #<Encoding:ASCII-8BIT>
89        // [3.1.2] > s = "abc"
90        // => "abc"
91        // [3.1.2] > s << b
92        // => "abc\xFF\xFE"
93        // [3.1.2] > s.encoding
94        // => #<Encoding:ASCII-8BIT>
95        // [3.1.2] > s = ""
96        // => ""
97        // [3.1.2] > b = "abc".b
98        // => "abc"
99        // [3.1.2] > b.ascii_only?
100        // => true
101        // [3.1.2] > s << b
102        // => "abc"
103        // [3.1.2] > s.encoding
104        // => #<Encoding:UTF-8>
105        // [3.1.2] > a = "\xFF\xFE".force_encoding(Encoding::ASCII)
106        // => "\xFF\xFE"
107        // [3.1.2] > s = ""
108        // => ""
109        // [3.1.2] > s << a
110        // => "\xFF\xFE"
111        // [3.1.2] > s.encoding
112        // => #<Encoding:US-ASCII>
113        // [3.1.2] > s = "abc"
114        // => "abc"
115        // [3.1.2] > s << a
116        // => "abc\xFF\xFE"
117        // [3.1.2] > s.encoding
118        // => #<Encoding:US-ASCII>
119        // ```
120        Encoding::Utf8 => {
121            // SAFETY: The string is repacked before any intervening uses of
122            // `interp` which means no mruby heap allocations can occur.
123            unsafe {
124                let string_mut = s.as_inner_mut();
125                // XXX: This call doesn't do a check to see if we'll exceed the max allocation
126                //    size and may panic or abort.
127                string_mut.extend_from_slice(other.as_slice());
128
129                if !matches!(other.encoding(), Encoding::Utf8) && !other.is_ascii_only() {
130                    // encodings are incompatible if other is not UTF-8 and is non-ASCII
131                    string_mut.set_encoding(other.encoding());
132                }
133
134                let s = s.take();
135                super::String::box_into_value(s, value, interp)
136            }
137        }
138        // Empty ASCII strings take on the encoding of the argument if the
139        // argument is not ASCII-compatible, even if the argument does not have
140        // a valid encoding.
141        //
142        // ```
143        // [3.1.2] > ae = "".force_encoding(Encoding::ASCII)
144        // => ""
145        // [3.1.2] > ae << "😀"
146        // => "😀"
147        // [3.1.2] > ae.encoding
148        // => #<Encoding:UTF-8>
149        // [3.1.2] > ae = "".force_encoding(Encoding::ASCII)
150        // => ""
151        // [3.1.2] > ae << "abc"
152        // => "abc"
153        // [3.1.2] > ae.encoding
154        // => #<Encoding:US-ASCII>
155        // [3.1.2] > ae = "".force_encoding(Encoding::ASCII)
156        // => ""
157        // [3.1.2] > ae << "\xFF\xFE"
158        // => "\xFF\xFE"
159        // [3.1.2] > ae.encoding
160        // => #<Encoding:UTF-8>
161        // [3.1.2] > ae = "".force_encoding(Encoding::ASCII)
162        // => ""
163        // [3.1.2] > ae << "\xFF\xFE".b
164        // => "\xFF\xFE"
165        // [3.1.2] > ae.encoding
166        // => #<Encoding:ASCII-8BIT>
167        // ```
168        Encoding::Ascii if s.is_empty() => {
169            // SAFETY: The string is repacked before any intervening uses of
170            // `interp` which means no mruby heap allocations can occur.
171            unsafe {
172                let string_mut = s.as_inner_mut();
173                // XXX: This call doesn't do a check to see if we'll exceed the max allocation
174                //    size and may panic or abort.
175                string_mut.extend_from_slice(other.as_slice());
176
177                // Set encoding to `other.encoding()` if other is non-ASCII.
178                if !other.is_ascii_only() {
179                    string_mut.set_encoding(other.encoding());
180                }
181
182                let s = s.take();
183                super::String::box_into_value(s, value, interp)
184            }
185        }
186        // ```
187        // [3.1.2] > a
188        // => "\xFF\xFE"
189        // [3.1.2] > a.encoding
190        // => #<Encoding:US-ASCII>
191        // [3.1.2] > a << "\xFF".b
192        // (irb):46:in `<main>': incompatible character encodings: US-ASCII and ASCII-8BIT (Encoding::CompatibilityError)
193        //         from /usr/local/var/rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/irb-1.4.1/exe/irb:11:in `<top (required)>'
194        //         from /usr/local/var/rbenv/versions/3.1.2/bin/irb:25:in `load'
195        //         from /usr/local/var/rbenv/versions/3.1.2/bin/irb:25:in `<main>'
196        // [3.1.2] > a << "abc"
197        // => "\xFF\xFEabc"
198        // [3.1.2] > s.encoding
199        // => #<Encoding:US-ASCII>
200        // [3.1.2] > a << "😀"
201        // (irb):49:in `<main>': incompatible character encodings: US-ASCII and UTF-8 (Encoding::CompatibilityError)
202        //         from /usr/local/var/rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/irb-1.4.1/exe/irb:11:in `<top (required)>'
203        //         from /usr/local/var/rbenv/versions/3.1.2/bin/irb:25:in `load'
204        //         from /usr/local/var/rbenv/versions/3.1.2/bin/irb:25:in `<main>'
205        // [3.1.2] > a << "😀".b
206        // (irb):50:in `<main>': incompatible character encodings: US-ASCII and ASCII-8BIT (Encoding::CompatibilityError)
207        //         from /usr/local/var/rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/irb-1.4.1/exe/irb:11:in `<top (required)>'
208        //         from /usr/local/var/rbenv/versions/3.1.2/bin/irb:25:in `load'
209        //         from /usr/local/var/rbenv/versions/3.1.2/bin/irb:25:in `<main>'
210        // ```
211        Encoding::Ascii => {
212            if !other.is_ascii() {
213                let code = format!(
214                    "raise Encoding::CompatibilityError, 'incompatible character encodings: {} and {}",
215                    s.encoding(),
216                    other.encoding()
217                );
218                interp.eval(code.as_bytes())?;
219                unreachable!("raised exception");
220            }
221            // SAFETY: The string is repacked before any intervening uses of
222            // `interp` which means no mruby heap allocations can occur.
223            unsafe {
224                let string_mut = s.as_inner_mut();
225                // XXX: This call doesn't do a check to see if we'll exceed the max allocation
226                //    size and may panic or abort.
227                string_mut.extend_from_slice(other.as_slice());
228
229                let s = s.take();
230                super::String::box_into_value(s, value, interp)
231            }
232        }
233        // If the receiver is an empty string with `Encoding::Binary` encoding
234        // and the argument is non-ASCII, take on the encoding of the argument.
235        //
236        // This requires the implicit conversion to string to return the
237        // underlying Spinoso string.
238        //
239        // ```
240        // [3.1.2] > be = "".b
241        // => ""
242        // [3.1.2] > be << "abc"
243        // => "abc"
244        // [3.1.2] > be.encoding
245        // => #<Encoding:ASCII-8BIT>
246        // [3.1.2] > be = "".b
247        // => ""
248        // [3.1.2] > be << "😀"
249        // => "😀"
250        // [3.1.2] > be.encoding
251        // => #<Encoding:UTF-8>
252        // [3.1.2] > be = "".b
253        // => ""
254        // [3.1.2] > be << "\xFF\xFE".force_encoding(Encoding::ASCII)
255        // => "\xFF\xFE"
256        // [3.1.2] > be.encoding
257        // => #<Encoding:US-ASCII>
258        // [3.1.2] > be = "".b
259        // => ""
260        // [3.1.2] > be << "abc".force_encoding(Encoding::ASCII)
261        // => "abc"
262        // [3.1.2] > be.encoding
263        // => #<Encoding:ASCII-8BIT>
264        // ```
265        Encoding::Binary if s.is_empty() => {
266            // SAFETY: The string is repacked before any intervening uses of
267            // `interp` which means no mruby heap allocations can occur.
268            unsafe {
269                let string_mut = s.as_inner_mut();
270                // XXX: This call doesn't do a check to see if we'll exceed the max allocation
271                //    size and may panic or abort.
272                string_mut.extend_from_slice(other.as_slice());
273
274                if !other.is_ascii_only() {
275                    string_mut.set_encoding(other.encoding());
276                }
277
278                let s = s.take();
279                super::String::box_into_value(s, value, interp)
280            }
281        }
282        Encoding::Binary => {
283            // SAFETY: The string is repacked before any intervening uses of
284            // `interp` which means no mruby heap allocations can occur.
285            unsafe {
286                let string_mut = s.as_inner_mut();
287                // XXX: This call doesn't do a check to see if we'll exceed the max allocation
288                //    size and may panic or abort.
289                string_mut.extend_from_slice(other.as_slice());
290
291                let s = s.take();
292                super::String::box_into_value(s, value, interp)
293            }
294        }
295    }
296}
297
298pub fn cmp_rocket(interp: &mut Artichoke, mut value: Value, mut other: Value) -> Result<Value, Error> {
299    let s = unsafe { super::String::unbox_from_value(&mut value, interp)? };
300    if let Ok(other) = unsafe { super::String::unbox_from_value(&mut other, interp) } {
301        let cmp = s.cmp(&*other);
302        Ok(interp.convert(cmp as i64))
303    } else {
304        Ok(Value::nil())
305    }
306}
307
308pub fn equals_equals(interp: &mut Artichoke, mut value: Value, mut other: Value) -> Result<Value, Error> {
309    let s = unsafe { super::String::unbox_from_value(&mut value, interp)? };
310    if let Ok(other) = unsafe { super::String::unbox_from_value(&mut other, interp) } {
311        let equals = *s == *other;
312        return Ok(interp.convert(equals));
313    }
314    if value.respond_to(interp, "to_str")? {
315        let result = other.funcall(interp, "==", &[value], None)?;
316        // any falsy returned value yields `false`, otherwise `true`.
317        if let Ok(result) = result.try_convert_into::<Option<bool>>(interp) {
318            let result = result.unwrap_or_default();
319            Ok(interp.convert(result))
320        } else {
321            Ok(interp.convert(true))
322        }
323    } else {
324        Ok(interp.convert(false))
325    }
326}
327
328#[allow(unused_mut)]
329#[expect(
330    clippy::cast_possible_wrap,
331    reason = "mruby stores sizes as int64_t instead of size_t"
332)]
333pub fn aref(
334    interp: &mut Artichoke,
335    mut value: Value,
336    mut first: Value,
337    second: Option<Value>,
338) -> Result<Value, Error> {
339    let s = unsafe { super::String::unbox_from_value(&mut value, interp)? };
340    if let Some(second) = second {
341        #[cfg(feature = "core-regexp")]
342        if let Ok(regexp) = unsafe { Regexp::unbox_from_value(&mut first, interp) } {
343            let match_data = regexp.match_(interp, Some(s.as_slice()), None, None)?;
344            if match_data.is_nil() {
345                return Ok(Value::nil());
346            }
347            return matchdata::trampoline::element_reference(interp, match_data, second, None);
348        }
349        let index = implicitly_convert_to_int(interp, first)?;
350        let length = implicitly_convert_to_int(interp, second)?;
351
352        let index = match aref::offset_to_index(index, s.len()) {
353            None => return Ok(Value::nil()),
354            // Short circuit with `nil` if `index > len`.
355            //
356            // ```
357            // [3.0.1] > s = "abc"
358            // => "abc"
359            // [3.0.1] > s[3, 10]
360            // => ""
361            // [3.0.1] > s[4, 10]
362            // => nil
363            // ```
364            //
365            // Don't specialize on the case where `index == len` because the provided
366            // length can change the result. Even if the length argument is not
367            // given, we still need to preserve the encoding of the source string,
368            // so fall through to the happy path below.
369            Some(index) if index > s.len() => return Ok(Value::nil()),
370            Some(index) => index,
371        };
372
373        // ```
374        // [3.0.1] > s = "abc"
375        // => "abc"
376        // [3.0.1] > s[1, -1]
377        // => nil
378        // ```
379        if let Ok(length) = usize::try_from(length) {
380            // ```
381            // [3.0.1] > s = "abc"
382            // => "abc"
383            // [3.0.1] > s[2**64, 2**64]
384            // (irb):26:in `[]': bignum too big to convert into `long' (RangeError)
385            // 	from (irb):26:in `<main>'
386            // 	from /usr/local/var/rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/irb-1.3.5/exe/irb:11:in `<top (required)>'
387            // 	from /usr/local/var/rbenv/versions/3.0.1/bin/irb:23:in `load'
388            // 	from /usr/local/var/rbenv/versions/3.0.1/bin/irb:23:in `<main>'
389            // ```
390            let end = index
391                .checked_add(length)
392                .ok_or_else(|| RangeError::with_message("bignum too big to convert into `long'"))?;
393            if let Some(slice) = s.get_char_slice(index..end) {
394                // Encoding from the source string is preserved.
395                //
396                // ```
397                // [3.0.1] > s = "abc"
398                // => "abc"
399                // [3.0.1] > s.encoding
400                // => #<Encoding:UTF-8>
401                // [3.0.1] > s[1, 2].encoding
402                // => #<Encoding:UTF-8>
403                // [3.0.1] > t = s.force_encoding(Encoding::ASCII)
404                // => "abc"
405                // [3.0.1] > t[1, 2].encoding
406                // => #<Encoding:US-ASCII>
407                // ```
408                let s = super::String::with_bytes_and_encoding(slice.to_vec(), s.encoding());
409                // ```
410                // [3.0.1] > class S < String; end
411                // => nil
412                // [3.0.1] > S.new("abc")[1, 2].class
413                // => String
414                // ```
415                //
416                // The returned `String` is never frozen:
417                //
418                // ```
419                // [3.0.1] > s = "abc"
420                // => "abc"
421                // [3.0.1] > s.frozen?
422                // => false
423                // [3.0.1] > s[1, 2].frozen?
424                // => false
425                // [3.0.1] > t = "abc".freeze
426                // => "abc"
427                // [3.0.1] > t[1, 2].frozen?
428                // => false
429                // ```
430                return super::String::alloc_value(s, interp);
431            }
432        }
433        return Ok(Value::nil());
434    }
435    #[cfg(feature = "core-regexp")]
436    if let Ok(regexp) = unsafe { Regexp::unbox_from_value(&mut first, interp) } {
437        let match_data = regexp.match_(interp, Some(s.as_slice()), None, None)?;
438        if match_data.is_nil() {
439            return Ok(Value::nil());
440        }
441        return matchdata::trampoline::element_reference(interp, match_data, interp.convert(0), None);
442    }
443    match first.is_range(interp, s.char_len() as i64)? {
444        None => {}
445        // ```
446        // [3.1.2] > ""[-1..-1]
447        // => nil
448        // ``
449        Some(protect::Range::Out) => return Ok(Value::nil()),
450        Some(protect::Range::Valid { start: index, len }) => {
451            let index = match aref::offset_to_index(index, s.len()) {
452                None => return Ok(Value::nil()),
453                Some(index) if index > s.len() => return Ok(Value::nil()),
454                Some(index) => index,
455            };
456            if let Ok(length) = usize::try_from(len) {
457                let end = index
458                    .checked_add(length)
459                    .ok_or_else(|| RangeError::with_message("bignum too big to convert into `long'"))?;
460                if let Some(slice) = s.get_char_slice(index..end) {
461                    let s = super::String::with_bytes_and_encoding(slice.to_vec(), s.encoding());
462                    return super::String::alloc_value(s, interp);
463                }
464            }
465            return Ok(Value::nil());
466        }
467    }
468    // The overload of `String#[]` that takes a `String` **only** takes `String`s.
469    // No implicit conversion is performed.
470    //
471    // ```
472    // [3.0.1] > s = "abc"
473    // => "abc"
474    // [3.0.1] > s["bc"]
475    // => "bc"
476    // [3.0.1] > class X; def to_str; "bc"; end; end
477    // => :to_str
478    // [3.0.1] > s[X.new]
479    // (irb):4:in `[]': no implicit conversion of X into Integer (TypeError)
480    // 	from (irb):4:in `<main>'
481    // 	from /usr/local/var/rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/irb-1.3.5/exe/irb:11:in `<top (required)>'
482    // 	from /usr/local/var/rbenv/versions/3.0.1/bin/irb:23:in `load'
483    // 	from /usr/local/var/rbenv/versions/3.0.1/bin/irb:23:in `<main>'
484    // 	```
485    if let Ok(substring) = unsafe { super::String::unbox_from_value(&mut first, interp) } {
486        if s.index(&*substring, None).is_some() {
487            // Indexing with a `String` returns a newly allocated object that
488            // has the same encoding as the index, regardless of the encoding on
489            // the receiver.
490            //
491            // ```
492            // [3.0.2] > s = "abc"
493            // => "abc"
494            // [3.0.2] > s.encoding
495            // => #<Encoding:UTF-8>
496            // [3.0.2] > s["bc"].encoding
497            // => #<Encoding:UTF-8>
498            // [3.0.2] > t = s.force_encoding(Encoding::ASCII_8BIT)
499            // => "abc"
500            // [3.0.2] > t.encoding
501            // => #<Encoding:ASCII-8BIT>
502            // [3.0.2] > t["bc"].encoding
503            // => #<Encoding:UTF-8>
504            // [3.0.2] > x = "bc"
505            // => "bc"
506            // [3.0.2] > x.encoding
507            // => #<Encoding:UTF-8>
508            // [3.0.2] > x.object_id
509            // => 260
510            // [3.0.2] > y = t[x]
511            // => "bc"
512            // [3.0.2] > y.encoding
513            // => #<Encoding:UTF-8>
514            // [3.0.2] > y.object_id
515            // => 280
516            // [3.0.2] > z = "bc".force_encoding(Encoding::ASCII)
517            // => "bc"
518            // [3.0.2] > y[z].encoding
519            // => #<Encoding:US-ASCII>
520            // [3.0.2] > t[z].encoding
521            // => #<Encoding:US-ASCII>
522            // [3.0.2] > s[z].encoding
523            // => #<Encoding:US-ASCII>
524            // ```
525            let encoding = substring.encoding();
526            let s = super::String::with_bytes_and_encoding(substring.to_vec(), encoding);
527            return super::String::alloc_value(s, interp);
528        }
529        return Ok(Value::nil());
530    }
531    let index = implicitly_convert_to_int(interp, first)?;
532
533    if let Some(index) = aref::offset_to_index(index, s.len()) {
534        // Index the byte, non existent indexes return `nil`.
535        //
536        // ```
537        // [3.0.1] > s = "abc"
538        // => "abc"
539        // [3.0.1] > s[2]
540        // => "c"
541        // [3.0.1] > s[3]
542        // => nil
543        // [3.0.1] > s[4]
544        // => nil
545        // ```
546        if let Some(bytes) = s.get_char(index) {
547            let s = super::String::with_bytes_and_encoding(bytes.to_vec(), s.encoding());
548            return super::String::alloc_value(s, interp);
549        }
550    }
551    Ok(Value::nil())
552}
553
554pub fn aset(interp: &mut Artichoke, mut value: Value) -> Result<Value, Error> {
555    check_frozen(interp, value)?;
556
557    let _s = unsafe { super::String::unbox_from_value(&mut value, interp)? };
558    Err(NotImplementedError::new().into())
559}
560
561pub fn is_ascii_only(interp: &mut Artichoke, mut value: Value) -> Result<Value, Error> {
562    let s = unsafe { super::String::unbox_from_value(&mut value, interp)? };
563    let is_ascii_only = s.is_ascii_only();
564    Ok(interp.convert(is_ascii_only))
565}
566
567pub fn b(interp: &mut Artichoke, mut value: Value) -> Result<Value, Error> {
568    let s = unsafe { super::String::unbox_from_value(&mut value, interp)? };
569    super::String::alloc_value(s.to_binary(), interp)
570}
571
572pub fn byteindex(
573    interp: &mut Artichoke,
574    mut value: Value,
575    mut substring: Value,
576    offset: Option<Value>,
577) -> Result<Value, Error> {
578    let s = unsafe { super::String::unbox_from_value(&mut value, interp)? };
579    #[cfg(feature = "core-regexp")]
580    if let Ok(pattern) = unsafe { Regexp::unbox_from_value(&mut substring, interp) } {
581        let offset_number = if let Some(offset) = offset {
582            Some(implicitly_convert_to_int(interp, offset)?)
583        } else {
584            None
585        };
586
587        // `offset_number` is a byte offset into the search string, not an
588        // offset into the capture group used. This is confirmed by playing
589        // around with MRI irb.
590        let mut matches = pattern.match_(interp, Some(s.as_slice()), offset_number, None)?;
591        if matches.is_nil() {
592            return Ok(Value::nil());
593        }
594        let first_ocurrence = unsafe { MatchData::unbox_from_value(&mut matches, interp)? };
595        let ocurrence_index = first_ocurrence.begin(matchdata::Capture::GroupIndex(0))?;
596        match ocurrence_index {
597            Some(n) => return interp.try_convert(n),
598            None => return Ok(Value::nil()),
599        }
600    }
601    let needle = unsafe { implicitly_convert_to_string(interp, &mut substring)? };
602    let offset = if let Some(offset) = offset {
603        let offset = implicitly_convert_to_int(interp, offset)?;
604        match aref::offset_to_index(offset, s.len()) {
605            None => return Ok(Value::nil()),
606            Some(offset) if offset > s.len() => return Ok(Value::nil()),
607            Some(offset) => Some(offset),
608        }
609    } else {
610        None
611    };
612    interp.try_convert(s.byteindex(needle, offset))
613}
614
615pub fn byterindex(
616    interp: &mut Artichoke,
617    mut value: Value,
618    mut substring: Value,
619    offset: Option<Value>,
620) -> Result<Value, Error> {
621    #[cfg(feature = "core-regexp")]
622    if let Ok(_pattern) = unsafe { Regexp::unbox_from_value(&mut substring, interp) } {
623        return Err(NotImplementedError::from("String#byterindex with Regexp pattern").into());
624    }
625    let s = unsafe { super::String::unbox_from_value(&mut value, interp)? };
626    let needle = unsafe { implicitly_convert_to_string(interp, &mut substring)? };
627    let offset = if let Some(offset) = offset {
628        let offset = implicitly_convert_to_int(interp, offset)?;
629        match aref::offset_to_index(offset, s.len()) {
630            None => return Ok(Value::nil()),
631            Some(offset) if offset > s.len() => return Ok(Value::nil()),
632            Some(offset) => Some(offset),
633        }
634    } else {
635        None
636    };
637    interp.try_convert(s.byterindex(needle, offset))
638}
639
640pub fn bytes(interp: &mut Artichoke, mut value: Value) -> Result<Value, Error> {
641    let s = unsafe { super::String::unbox_from_value(&mut value, interp)? };
642    let bytes = s
643        .bytes()
644        .map(i64::from)
645        .map(|byte| interp.convert(byte))
646        .collect::<Array>();
647    Array::alloc_value(bytes, interp)
648}
649
650pub fn bytesize(interp: &mut Artichoke, mut value: Value) -> Result<Value, Error> {
651    let s = unsafe { super::String::unbox_from_value(&mut value, interp)? };
652    let bytesize = s.bytesize();
653    interp.try_convert(bytesize)
654}
655
656#[expect(
657    clippy::cast_possible_wrap,
658    reason = "mruby stores sizes as int64_t instead of size_t"
659)]
660pub fn byteslice(
661    interp: &mut Artichoke,
662    mut value: Value,
663    index: Value,
664    length: Option<Value>,
665) -> Result<Value, Error> {
666    let s = unsafe { super::String::unbox_from_value(&mut value, interp)? };
667    let maybe_range = if length.is_none() {
668        index.is_range(interp, s.bytesize() as i64)?
669    } else {
670        None
671    };
672    match maybe_range {
673        None => {}
674        Some(protect::Range::Out) => return Ok(Value::nil()),
675        Some(protect::Range::Valid { start: index, len }) => {
676            let index = match aref::offset_to_index(index, s.len()) {
677                None => return Ok(Value::nil()),
678                Some(index) if index > s.len() => return Ok(Value::nil()),
679                Some(index) => index,
680            };
681            if let Ok(length) = usize::try_from(len) {
682                let end = index
683                    .checked_add(length)
684                    .ok_or_else(|| RangeError::with_message("bignum too big to convert into `long'"))?;
685                if let Some(slice) = s.get(index..end).or_else(|| s.get(index..)) {
686                    // Encoding from the source string is preserved.
687                    //
688                    // ```
689                    // [3.1.2] > s = "abc"
690                    // => "abc"
691                    // [3.1.2] > s.encoding
692                    // => #<Encoding:UTF-8>
693                    // [3.1.2] > s.byteslice(1..3).encoding
694                    // => #<Encoding:UTF-8>
695                    // [3.1.2] > t = s.force_encoding(Encoding::ASCII)
696                    // => "abc"
697                    // [3.1.2] > t.byteslice(1..3).encoding
698                    // => #<Encoding:US-ASCII>
699                    // ```
700                    let s = super::String::with_bytes_and_encoding(slice.to_vec(), s.encoding());
701                    // ```
702                    // [3.1.2] > class S < String; end
703                    // => nil
704                    // [3.1.2] > S.new("abc").byteslice(1..3).class
705                    // => String
706                    // ```
707                    //
708                    // The returned `String` is never frozen:
709                    //
710                    // ```
711                    // [3.1.2] > s = "abc"
712                    // => "abc"
713                    // [3.1.2] > s.frozen?
714                    // => false
715                    // [3.1.2] > s.byteslice(1..3).frozen?
716                    // => false
717                    // [3.1.2] > t = "abc".freeze
718                    // => "abc"
719                    // [3.1.2] > t.byteslice(1..3).frozen?
720                    // => false
721                    // ```
722                    return super::String::alloc_value(s, interp);
723                }
724            }
725        }
726    }
727    // ```
728    // [3.0.2] > class A; def to_int; 1; end; end
729    // => :to_int
730    // [3.0.2] > s = "abc"
731    // => "abc"
732    // [3.0.2] > s.byteslice(A.new)
733    // => "b"
734    // [3.0.2] > s.byteslice(//)
735    // (irb):16:in `byteslice': no implicit conversion of Regexp into Integer (TypeError)
736    // 	from (irb):16:in `<main>'
737    // 	from /usr/local/var/rbenv/versions/3.0.2/lib/ruby/gems/3.0.0/gems/irb-1.3.5/exe/irb:11:in `<top (required)>'
738    // 	from /usr/local/var/rbenv/versions/3.0.2/bin/irb:23:in `load'
739    // 	from /usr/local/var/rbenv/versions/3.0.2/bin/irb:23:in `<main>'
740    // ```
741    let index = implicitly_convert_to_int(interp, index)?;
742
743    let index = match aref::offset_to_index(index, s.len()) {
744        None => return Ok(Value::nil()),
745        // Short circuit with `nil` if `index > len`.
746        //
747        // ```
748        // [3.0.1] > s = "abc"
749        // => "abc"
750        // [3.0.1] > s.byteslice(3, 10)
751        // => ""
752        // [3.0.1] > s.byteslice(4, 10)
753        // => nil
754        // ```
755        //
756        // Don't specialize on the case where `index == len` because the provided
757        // length can change the result. Even if the length argument is not
758        // given, we still need to preserve the encoding of the source string,
759        // so fall through to the happy path below.
760        Some(index) if index > s.len() => return Ok(Value::nil()),
761        Some(index) => index,
762    };
763
764    let length = if let Some(length) = length {
765        length
766    } else {
767        // Per the docs -- https://ruby-doc.org/core-3.1.2/String.html#method-i-byteslice
768        //
769        // > If passed a single Integer, returns a substring of one byte at that position.
770        //
771        // NOTE: Index out a single byte rather than a slice to avoid having
772        // to do an overflow check on the addition.
773        if let Some(&byte) = s.get(index) {
774            let s = super::String::with_bytes_and_encoding(vec![byte], s.encoding());
775            // ```
776            // [3.0.1] > class S < String; end
777            // => nil
778            // [3.0.1] > S.new("abc").byteslice(1, 2).class
779            // => String
780            // ```
781            //
782            // The returned `String` is never frozen:
783            //
784            // ```
785            // [3.0.1] > s = "abc"
786            // => "abc"
787            // [3.0.1] > s.frozen?
788            // => false
789            // [3.0.1] > s.byteslice(1, 2).frozen?
790            // => false
791            // [3.0.1] > t = "abc".freeze
792            // => "abc"
793            // [3.0.1] > t.byteslice(1, 2).frozen?
794            // => false
795            // ```
796            return super::String::alloc_value(s, interp);
797        }
798        return Ok(Value::nil());
799    };
800
801    // ```
802    // [3.0.2] > class A; def to_int; 1; end; end
803    // => :to_int
804    // [3.0.2] > s = "abc"
805    // => "abc"
806    // [3.0.2] > s.byteslice(A.new)
807    // => "b"
808    // [3.0.2] > s.byteslice(A.new, A.new)
809    // => "b"
810    // [3.0.2] > s.byteslice(2, //)
811    // (irb):17:in `byteslice': no implicit conversion of Regexp into Integer (TypeError)
812    // 	from (irb):17:in `<main>'
813    // 	from /usr/local/var/rbenv/versions/3.0.2/lib/ruby/gems/3.0.0/gems/irb-1.3.5/exe/irb:11:in `<top (required)>'
814    // 	from /usr/local/var/rbenv/versions/3.0.2/bin/irb:23:in `load'
815    // 	from /usr/local/var/rbenv/versions/3.0.2/bin/irb:23:in `<main>
816    // ```
817    let length = implicitly_convert_to_int(interp, length)?;
818
819    // ```
820    // [3.0.2] > s = "abc"
821    // => "abc"
822    // [3.0.2] > s.byteslice(1, -1)
823    // => nil
824    // ```
825    if let Ok(length) = usize::try_from(length) {
826        // ```
827        // [3.0.2] > s = "abc"
828        // => "abc"
829        // [3.0.2] > s.byteslice(2**64, 2**64)
830        // (irb):38:in `byteslice': bignum too big to convert into `long' (RangeError)
831        // 	from (irb):38:in `<main>'
832        // 	from /usr/local/var/rbenv/versions/3.0.2/lib/ruby/gems/3.0.0/gems/irb-1.3.5/exe/irb:11:in `<top (required)>'
833        // 	from /usr/local/var/rbenv/versions/3.0.2/bin/irb:23:in `load'
834        // 	from /usr/local/var/rbenv/versions/3.0.2/bin/irb:23:in `<main>'
835        // ```
836        let end = index
837            .checked_add(length)
838            .ok_or_else(|| RangeError::with_message("bignum too big to convert into `long'"))?;
839        if let Some(slice) = s.get(index..end).or_else(|| s.get(index..)) {
840            // Encoding from the source string is preserved.
841            //
842            // ```
843            // [3.0.2] > s = "abc"
844            // => "abc"
845            // [3.0.2] > s.encoding
846            // => #<Encoding:UTF-8>
847            // [3.0.2] > s.byteslice(1, 2).encoding
848            // => #<Encoding:UTF-8>
849            // [3.0.2] > t = s.force_encoding(Encoding::ASCII)
850            // => "abc"
851            // [3.0.2] > t.byteslice(1, 2).encoding
852            // => #<Encoding:US-ASCII>
853            // ```
854            let s = super::String::with_bytes_and_encoding(slice.to_vec(), s.encoding());
855            // ```
856            // [3.0.1] > class S < String; end
857            // => nil
858            // [3.0.1] > S.new("abc").byteslice(1, 2).class
859            // => String
860            // ```
861            //
862            // The returned `String` is never frozen:
863            //
864            // ```
865            // [3.0.1] > s = "abc"
866            // => "abc"
867            // [3.0.1] > s.frozen?
868            // => false
869            // [3.0.1] > s.byteslice(1, 2).frozen?
870            // => false
871            // [3.0.1] > t = "abc".freeze
872            // => "abc"
873            // [3.0.1] > t.byteslice(1, 2).frozen?
874            // => false
875            // ```
876            return super::String::alloc_value(s, interp);
877        }
878    }
879    Ok(Value::nil())
880}
881
882pub fn capitalize(interp: &mut Artichoke, mut value: Value) -> Result<Value, Error> {
883    let s = unsafe { super::String::unbox_from_value(&mut value, interp)? };
884    let mut dup = s.clone();
885    dup.make_capitalized();
886    super::String::alloc_value(dup, interp)
887}
888
889pub fn capitalize_bang(interp: &mut Artichoke, mut value: Value) -> Result<Value, Error> {
890    check_frozen(interp, value)?;
891
892    let mut s = unsafe { super::String::unbox_from_value(&mut value, interp)? };
893    // SAFETY: The string is repacked before any intervening uses of `interp`
894    // which means no mruby heap allocations can occur.
895    let (effect, returned) = unsafe {
896        let string_mut = s.as_inner_mut();
897        // `make_capitalized` might reallocate the string and invalidate the
898        // boxed pointer, capacity, length triple.
899        let effect = string_mut.make_capitalized();
900
901        let s = s.take();
902        (effect, super::String::box_into_value(s, value, interp)?)
903    };
904    if effect.changed() {
905        Ok(returned)
906    } else {
907        Ok(Value::nil())
908    }
909}
910
911pub fn casecmp_ascii(interp: &mut Artichoke, mut value: Value, mut other: Value) -> Result<Value, Error> {
912    let s = unsafe { super::String::unbox_from_value(&mut value, interp)? };
913    // SAFETY: The byte slice is immediately discarded after extraction. There
914    // are no intervening interpreter accesses.
915    if let Ok(other) = unsafe { implicitly_convert_to_string(interp, &mut other) } {
916        let cmp = s.ascii_casecmp(other) as i64;
917        Ok(interp.convert(cmp))
918    } else {
919        Ok(Value::nil())
920    }
921}
922
923pub fn casecmp_unicode(interp: &mut Artichoke, mut value: Value, mut other: Value) -> Result<Value, Error> {
924    let s = unsafe { super::String::unbox_from_value(&mut value, interp)? };
925    // TODO: this needs to do an implicit conversion, but we need a Spinoso string.
926    if let Ok(other) = unsafe { super::String::unbox_from_value(&mut other, interp) } {
927        let eql = *s == *other;
928        Ok(interp.convert(eql))
929    } else {
930        Ok(interp.convert(false))
931    }
932}
933
934pub fn center(interp: &mut Artichoke, mut value: Value, width: Value, padstr: Option<Value>) -> Result<Value, Error> {
935    let s = unsafe { super::String::unbox_from_value(&mut value, interp)? };
936    let width = implicitly_convert_to_int(interp, width)?;
937    let width = if let Ok(width) = usize::try_from(width) {
938        width
939    } else {
940        // ```ruby
941        // [3.0.2] > "a".center(-1)
942        // => "a"
943        // [3.0.2] > "a".center(-10)
944        // => "a"
945        // [3.0.2] > x = "a"
946        // => "a"
947        // [3.0.2] > y = "a".center(-10)
948        // => "a"
949        // [3.0.2] > x.object_id == y.object_id
950        // => false
951        // ```
952        let dup = s.clone();
953        return super::String::alloc_value(dup, interp);
954    };
955    // SAFETY: The byte slice is immediately converted to an owned `Vec` after
956    // extraction. There are no intervening interpreter accesses.
957    let padstr = if let Some(mut padstr) = padstr {
958        let padstr = unsafe { implicitly_convert_to_string(interp, &mut padstr)? };
959        Some(padstr.to_vec())
960    } else {
961        None
962    };
963    let centered = s
964        .center(width, padstr.as_deref())
965        .map_err(|e| ArgumentError::with_message(e.message()))?
966        .collect::<super::String>();
967    super::String::alloc_value(centered, interp)
968}
969
970pub fn chars(interp: &mut Artichoke, mut value: Value) -> Result<Value, Error> {
971    let s = unsafe { super::String::unbox_from_value(&mut value, interp)? };
972    let chars = s.chars().collect::<Vec<&[u8]>>();
973    interp.try_convert_mut(chars)
974}
975
976pub fn chomp(interp: &mut Artichoke, mut value: Value, separator: Option<Value>) -> Result<Value, Error> {
977    let s = unsafe { super::String::unbox_from_value(&mut value, interp)? };
978    let mut dup = s.clone();
979    if let Some(mut separator) = separator {
980        if let Some(sep) = unsafe { implicitly_convert_to_nilable_string(interp, &mut separator)? } {
981            let _ = dup.chomp(Some(sep));
982        } else {
983            return interp.try_convert_mut("");
984        }
985    } else {
986        let _ = dup.chomp(None::<&[u8]>);
987    }
988    super::String::alloc_value(dup, interp)
989}
990
991pub fn chomp_bang(interp: &mut Artichoke, mut value: Value, separator: Option<Value>) -> Result<Value, Error> {
992    check_frozen(interp, value)?;
993
994    let mut s = unsafe { super::String::unbox_from_value(&mut value, interp)? };
995    unsafe {
996        let string_mut = s.as_inner_mut();
997        let modified = if let Some(mut separator) = separator {
998            if let Some(sep) = implicitly_convert_to_nilable_string(interp, &mut separator)? {
999                string_mut.chomp(Some(sep))
1000            } else {
1001                return Ok(Value::nil());
1002            }
1003        } else {
1004            string_mut.chomp(None::<&[u8]>)
1005        };
1006        if modified {
1007            let s = s.take();
1008            return super::String::box_into_value(s, value, interp);
1009        }
1010    }
1011    Ok(Value::nil())
1012}
1013
1014pub fn chop(interp: &mut Artichoke, mut value: Value) -> Result<Value, Error> {
1015    let s = unsafe { super::String::unbox_from_value(&mut value, interp)? };
1016    let mut dup = s.clone();
1017    let _ = dup.chop();
1018    super::String::alloc_value(dup, interp)
1019}
1020
1021pub fn chop_bang(interp: &mut Artichoke, mut value: Value) -> Result<Value, Error> {
1022    check_frozen(interp, value)?;
1023
1024    let mut s = unsafe { super::String::unbox_from_value(&mut value, interp)? };
1025    if s.is_empty() {
1026        return Ok(Value::nil());
1027    }
1028    unsafe {
1029        let string_mut = s.as_inner_mut();
1030        let modified = string_mut.chop();
1031        if modified {
1032            let s = s.take();
1033            return super::String::box_into_value(s, value, interp);
1034        }
1035    }
1036    Ok(value)
1037}
1038
1039pub fn chr(interp: &mut Artichoke, mut value: Value) -> Result<Value, Error> {
1040    let s = unsafe { super::String::unbox_from_value(&mut value, interp)? };
1041    let chr = s.chr();
1042    interp.try_convert_mut(chr)
1043}
1044
1045pub fn clear(interp: &mut Artichoke, mut value: Value) -> Result<Value, Error> {
1046    check_frozen(interp, value)?;
1047
1048    let mut s = unsafe { super::String::unbox_from_value(&mut value, interp)? };
1049    // SAFETY: The string is repacked before any intervening uses of `interp`
1050    // which means no mruby heap allocations can occur.
1051    unsafe {
1052        let string_mut = s.as_inner_mut();
1053        string_mut.clear();
1054
1055        let s = s.take();
1056        super::String::box_into_value(s, value, interp)
1057    }
1058}
1059
1060pub fn codepoints(interp: &mut Artichoke, mut value: Value) -> Result<Value, Error> {
1061    let s = unsafe { super::String::unbox_from_value(&mut value, interp)? };
1062    let codepoints = s
1063        .codepoints()
1064        .map_err(|err| ArgumentError::with_message(err.message()))?;
1065    let codepoints = codepoints.map(|ch| interp.convert(ch)).collect::<Array>();
1066    Array::alloc_value(codepoints, interp)
1067}
1068
1069pub fn downcase(interp: &mut Artichoke, mut value: Value) -> Result<Value, Error> {
1070    let s = unsafe { super::String::unbox_from_value(&mut value, interp)? };
1071    let mut dup = s.clone();
1072    dup.make_lowercase();
1073    super::String::alloc_value(dup, interp)
1074}
1075
1076pub fn downcase_bang(interp: &mut Artichoke, mut value: Value) -> Result<Value, Error> {
1077    check_frozen(interp, value)?;
1078
1079    let mut s = unsafe { super::String::unbox_from_value(&mut value, interp)? };
1080    // SAFETY: The string is repacked before any intervening uses of `interp`
1081    // which means no mruby heap allocations can occur.
1082    let (effect, returned) = unsafe {
1083        let string_mut = s.as_inner_mut();
1084        // `make_lowercase` might reallocate the string and invalidate the boxed
1085        // pointer, capacity, length triple.
1086        let effect = string_mut.make_lowercase();
1087
1088        let s = s.take();
1089        (effect, super::String::box_into_value(s, value, interp)?)
1090    };
1091    if effect.changed() {
1092        Ok(returned)
1093    } else {
1094        Ok(Value::nil())
1095    }
1096}
1097
1098pub fn is_empty(interp: &mut Artichoke, mut value: Value) -> Result<Value, Error> {
1099    let s = unsafe { super::String::unbox_from_value(&mut value, interp)? };
1100    Ok(interp.convert(s.is_empty()))
1101}
1102
1103pub fn end_with<T>(interp: &mut Artichoke, mut value: Value, suffixes: T) -> Result<Value, Error>
1104where
1105    T: IntoIterator<Item = Value>,
1106{
1107    let s = unsafe { super::String::unbox_from_value(&mut value, interp)? };
1108
1109    for mut suffix in suffixes {
1110        // SAFETY: `s` used and discarded immediately before any intervening operations on the VM.
1111        // This ensures there are no intervening garbage collections which may free the `RString*` that backs this value.
1112        let needle = unsafe { implicitly_convert_to_string(interp, &mut suffix)? };
1113        if s.ends_with(needle) {
1114            return Ok(interp.convert(true));
1115        }
1116    }
1117    Ok(interp.convert(false))
1118}
1119
1120pub fn eql(interp: &mut Artichoke, mut value: Value, mut other: Value) -> Result<Value, Error> {
1121    let s = unsafe { super::String::unbox_from_value(&mut value, interp)? };
1122    if let Ok(other) = unsafe { super::String::unbox_from_value(&mut other, interp) } {
1123        let eql = *s == *other;
1124        Ok(interp.convert(eql))
1125    } else {
1126        Ok(interp.convert(false))
1127    }
1128}
1129
1130pub fn getbyte(interp: &mut Artichoke, mut value: Value, index: Value) -> Result<Value, Error> {
1131    let s = unsafe { super::String::unbox_from_value(&mut value, interp)? };
1132    let index = implicitly_convert_to_int(interp, index)?;
1133    let index = if let Some(index) = aref::offset_to_index(index, s.len()) {
1134        index
1135    } else {
1136        return Ok(Value::nil());
1137    };
1138    let byte = s.get(index).copied().map(i64::from);
1139    Ok(interp.convert(byte))
1140}
1141
1142pub fn hash(interp: &mut Artichoke, mut value: Value) -> Result<Value, Error> {
1143    let s = unsafe { super::String::unbox_from_value(&mut value, interp)? };
1144    let hash = interp.global_build_hasher()?.hash_one(s.as_slice());
1145    // bit cast to an `i64` to ensure the value is signed. We're computing a
1146    // hash so the sign of the value is not important.
1147    let hash = i64::from_ne_bytes(hash.to_ne_bytes());
1148    Ok(interp.convert(hash))
1149}
1150
1151pub fn include(interp: &mut Artichoke, mut value: Value, mut other: Value) -> Result<Value, Error> {
1152    let s = unsafe { super::String::unbox_from_value(&mut value, interp)? };
1153    let other_str = unsafe { implicitly_convert_to_string(interp, &mut other)? };
1154    let includes = s.index(other_str, None).is_some();
1155    Ok(interp.convert(includes))
1156}
1157
1158pub fn index(
1159    interp: &mut Artichoke,
1160    mut value: Value,
1161    mut needle: Value,
1162    offset: Option<Value>,
1163) -> Result<Value, Error> {
1164    let s = unsafe { super::String::unbox_from_value(&mut value, interp)? };
1165    #[cfg(feature = "core-regexp")]
1166    if let Ok(_pattern) = unsafe { Regexp::unbox_from_value(&mut needle, interp) } {
1167        return Err(NotImplementedError::from("String#index with Regexp pattern").into());
1168    }
1169    let needle = unsafe { implicitly_convert_to_string(interp, &mut needle)? };
1170    let index = if let Some(offset) = offset {
1171        let offset = implicitly_convert_to_int(interp, offset)?;
1172        let offset = aref::offset_to_index(offset, s.len());
1173        s.index(needle, offset)
1174    } else {
1175        s.index(needle, None)
1176    };
1177    let index = index.and_then(|index| i64::try_from(index).ok());
1178    interp.try_convert(index)
1179}
1180
1181pub fn initialize(interp: &mut Artichoke, mut value: Value, from: Option<Value>) -> Result<Value, Error> {
1182    if from.is_some() {
1183        check_frozen(interp, value)?;
1184    }
1185
1186    let Some(mut from) = from else {
1187        // Calling `initialize` on an already initialized `String` with no arguments
1188        // is a no-op. If the string is not yet initialized, we will subsequently
1189        // initialize it when unpacking the `Value` in the `String::unbox_from_value`
1190        // call.
1191        return Ok(value);
1192    };
1193
1194    // We must convert `from` to a byte buffer first in case `#to_str` raises.
1195    //
1196    // If we don't, the following scenario could leave `value` with a dangling
1197    // pointer.
1198    //
1199    // ```console
1200    // [3.0.2] > s = "abc"
1201    // => "abc"
1202    // [3.0.2] > class B; def to_str; raise 'oh no'; end; end
1203    // => :to_str
1204    // [3.0.2] > s.send(:initialize, B.new)
1205    // (irb):6:in `to_str': oh no (RuntimeError)
1206    // 	from (irb):7:in `initialize'
1207    // 	from (irb):7:in `<main>'
1208    // 	from /usr/local/var/rbenv/versions/3.0.2/lib/ruby/gems/3.0.0/gems/irb-1.3.5/exe/irb:11:in `<top (required)>'
1209    // 	from /usr/local/var/rbenv/versions/3.0.2/bin/irb:23:in `load'
1210    // 	from /usr/local/var/rbenv/versions/3.0.2/bin/irb:23:in `<main>'
1211    // [3.0.2] > s
1212    // => "abc"
1213    // ```
1214    // SAFETY: The extracted slice is immediately copied to an owned buffer.
1215    // No intervening operations on the mruby VM occur.
1216    let from = unsafe { implicitly_convert_to_string(interp, &mut from)? };
1217    let buf = from.to_vec();
1218    // If we are calling `initialize` on an already initialized `String`,
1219    // pluck out the inner buffer and drop it so we don't leak memory.
1220    //
1221    // ```console
1222    // [3.0.2] > s = "abc"
1223    // => "abc"
1224    // [3.0.2] > class A; def to_str; 'foo'; end; end
1225    // => :to_str
1226    // [3.0.2] > s.send(:initialize, A.new)
1227    // => "foo"
1228    // [3.0.2] > s
1229    // => "foo"
1230    // ```
1231    if let Ok(s) = unsafe { super::String::unbox_from_value(&mut value, interp) } {
1232        unsafe {
1233            let inner = s.take();
1234            drop(inner);
1235        }
1236    }
1237    // We are no guaranteed that the `buf` pointer in `value` is either dangling
1238    // or uninitialized. We will ensure that we box the given bytes into it.
1239    let buf = super::String::from(buf);
1240    super::String::box_into_value(buf, value, interp)
1241}
1242
1243pub fn initialize_copy(interp: &mut Artichoke, mut value: Value, mut other: Value) -> Result<Value, Error> {
1244    check_frozen(interp, value)?;
1245
1246    // SAFETY: The extracted slice is immediately copied to an owned buffer. No
1247    // intervening operations on the mruby VM occur.
1248    let buf = unsafe {
1249        let from = implicitly_convert_to_string(interp, &mut other)?;
1250        from.to_vec()
1251    };
1252    // If we are calling `initialize_copy` on an already initialized `String`,
1253    // pluck out the inner buffer and drop it so we don't leak memory.
1254    if let Ok(s) = unsafe { super::String::unbox_from_value(&mut value, interp) } {
1255        unsafe {
1256            let inner = s.take();
1257            drop(inner);
1258        }
1259    }
1260    // XXX: This should use the encoding of the given `other`.
1261    let replacement = super::String::with_bytes_and_encoding(buf, Encoding::Utf8);
1262    super::String::box_into_value(replacement, value, interp)
1263}
1264
1265pub fn inspect(interp: &mut Artichoke, mut value: Value) -> Result<Value, Error> {
1266    let s = unsafe { super::String::unbox_from_value(&mut value, interp)? };
1267    let inspect = s.inspect().collect::<super::String>();
1268    super::String::alloc_value(inspect, interp)
1269}
1270
1271pub fn intern(interp: &mut Artichoke, mut value: Value) -> Result<Value, Error> {
1272    let s = unsafe { super::String::unbox_from_value(&mut value, interp)? };
1273    let bytes = s.as_slice();
1274    let sym = if let Some(sym) = interp.check_interned_bytes(bytes)? {
1275        sym
1276    } else {
1277        interp.intern_bytes(bytes.to_vec())?
1278    };
1279    Symbol::alloc_value(sym.into(), interp)
1280}
1281
1282pub fn length(interp: &mut Artichoke, mut value: Value) -> Result<Value, Error> {
1283    let s = unsafe { super::String::unbox_from_value(&mut value, interp)? };
1284    let length = s.char_len();
1285    interp.try_convert(length)
1286}
1287
1288pub fn ord(interp: &mut Artichoke, mut value: Value) -> Result<Value, Error> {
1289    let s = unsafe { super::String::unbox_from_value(&mut value, interp)? };
1290    let ord = s.ord().map_err(|err| ArgumentError::with_message(err.message()))?;
1291    Ok(interp.convert(ord))
1292}
1293
1294pub fn replace(interp: &mut Artichoke, value: Value, other: Value) -> Result<Value, Error> {
1295    initialize_copy(interp, value, other)
1296}
1297
1298pub fn reverse(interp: &mut Artichoke, mut value: Value) -> Result<Value, Error> {
1299    let s = unsafe { super::String::unbox_from_value(&mut value, interp)? };
1300    let mut reversed = s.clone();
1301    reversed.reverse();
1302    super::String::alloc_value(reversed, interp)
1303}
1304
1305pub fn reverse_bang(interp: &mut Artichoke, mut value: Value) -> Result<Value, Error> {
1306    check_frozen(interp, value)?;
1307
1308    let mut s = unsafe { super::String::unbox_from_value(&mut value, interp)? };
1309    // SAFETY: The string is repacked before any intervening uses of `interp`
1310    // which means no mruby heap allocations can occur.
1311    unsafe {
1312        let string_mut = s.as_inner_mut();
1313        string_mut.reverse();
1314
1315        let s = s.take();
1316        super::String::box_into_value(s, value, interp)
1317    }
1318}
1319
1320pub fn rindex(
1321    interp: &mut Artichoke,
1322    mut value: Value,
1323    mut needle: Value,
1324    offset: Option<Value>,
1325) -> Result<Value, Error> {
1326    let s = unsafe { super::String::unbox_from_value(&mut value, interp)? };
1327    #[cfg(feature = "core-regexp")]
1328    if let Ok(_pattern) = unsafe { Regexp::unbox_from_value(&mut needle, interp) } {
1329        return Err(NotImplementedError::from("String#index with Regexp pattern").into());
1330    }
1331    let needle = unsafe { implicitly_convert_to_string(interp, &mut needle)? };
1332    let index = if let Some(offset) = offset {
1333        let offset = implicitly_convert_to_int(interp, offset)?;
1334        let offset = aref::offset_to_index(offset, s.len());
1335        s.rindex(needle, offset)
1336    } else {
1337        s.rindex(needle, None)
1338    };
1339    let index = index.and_then(|index| i64::try_from(index).ok());
1340    interp.try_convert(index)
1341}
1342
1343pub fn scan(interp: &mut Artichoke, value: Value, mut pattern: Value, block: Option<Block>) -> Result<Value, Error> {
1344    if let Ruby::Symbol = pattern.ruby_type() {
1345        let mut message = String::from("wrong argument type ");
1346        message.push_str(interp.inspect_type_name_for_value(pattern));
1347        message.push_str(" (expected Regexp)");
1348        return Err(TypeError::from(message).into());
1349    }
1350    #[cfg(feature = "core-regexp")]
1351    if let Ok(regexp) = unsafe { Regexp::unbox_from_value(&mut pattern, interp) } {
1352        let haystack = value.try_convert_into_mut::<&[u8]>(interp)?;
1353        let scan = regexp.inner().scan(interp, haystack, block)?;
1354        return Ok(interp.try_convert_mut(scan)?.unwrap_or(value));
1355    }
1356    #[cfg(feature = "core-regexp")]
1357    // SAFETY: `pattern_bytes` is converted to an owned byte vec to ensure the
1358    // underlying `RString*` is not garbage collected when yielding matches.
1359    if let Ok(pattern_bytes) = unsafe { implicitly_convert_to_string(interp, &mut pattern) } {
1360        let pattern_bytes = pattern_bytes.to_vec();
1361
1362        let string = value.try_convert_into_mut::<&[u8]>(interp)?;
1363        if let Some(ref block) = block {
1364            let regex = Regexp::try_from(pattern_bytes.clone())?;
1365            let matchdata = MatchData::new(string.to_vec(), regex, ..);
1366            let patlen = pattern_bytes.len();
1367            if let Some(pos) = string.find(&pattern_bytes) {
1368                let mut data = matchdata.clone();
1369                data.set_region(pos..pos + patlen);
1370                let data = MatchData::alloc_value(data, interp)?;
1371                interp.set_global_variable(regexp::LAST_MATCH, &data)?;
1372
1373                let block_arg = interp.try_convert_mut(pattern_bytes.as_slice())?;
1374                block.yield_arg(interp, &block_arg)?;
1375
1376                interp.set_global_variable(regexp::LAST_MATCH, &data)?;
1377
1378                let offset = pos + patlen;
1379                let string = string.get(offset..).unwrap_or_default();
1380                for pos in string.find_iter(&pattern_bytes) {
1381                    let mut data = matchdata.clone();
1382                    data.set_region(offset + pos..offset + pos + patlen);
1383                    let data = MatchData::alloc_value(data, interp)?;
1384                    interp.set_global_variable(regexp::LAST_MATCH, &data)?;
1385
1386                    let block_arg = interp.try_convert_mut(pattern_bytes.as_slice())?;
1387                    block.yield_arg(interp, &block_arg)?;
1388
1389                    interp.set_global_variable(regexp::LAST_MATCH, &data)?;
1390                }
1391            } else {
1392                interp.unset_global_variable(regexp::LAST_MATCH)?;
1393            }
1394            return Ok(value);
1395        }
1396        let (matches, last_pos) = string
1397            .find_iter(&pattern_bytes)
1398            .enumerate()
1399            .last()
1400            .map(|(m, p)| (m + 1, p))
1401            .unwrap_or_default();
1402        let mut result = Vec::with_capacity(matches);
1403        for _ in 0..matches {
1404            result.push(interp.try_convert_mut(pattern_bytes.as_slice())?);
1405        }
1406        if matches > 0 {
1407            let regex = Regexp::try_from(pattern_bytes.clone())?;
1408            let matchdata = MatchData::new(string.to_vec(), regex, last_pos..last_pos + pattern_bytes.len());
1409            let data = MatchData::alloc_value(matchdata, interp)?;
1410            interp.set_global_variable(regexp::LAST_MATCH, &data)?;
1411        } else {
1412            interp.unset_global_variable(regexp::LAST_MATCH)?;
1413        }
1414        return interp.try_convert_mut(result);
1415    }
1416    #[cfg(not(feature = "core-regexp"))]
1417    // SAFETY: `pattern_bytes` is converted to an owned byte vec to ensure the
1418    // underlying `RString*` is not garbage collected when yielding matches.
1419    if let Ok(pattern_bytes) = unsafe { implicitly_convert_to_string(interp, &mut pattern) } {
1420        let pattern_bytes = pattern_bytes.to_vec();
1421
1422        let string = value.try_convert_into_mut::<&[u8]>(interp)?;
1423        if let Some(ref block) = block {
1424            let patlen = pattern_bytes.len();
1425            if let Some(pos) = string.find(&pattern_bytes) {
1426                let block_arg = interp.try_convert_mut(pattern_bytes.as_slice())?;
1427                block.yield_arg(interp, &block_arg)?;
1428
1429                let offset = pos + patlen;
1430                let string = string.get(offset..).unwrap_or_default();
1431                for _ in string.find_iter(&pattern_bytes) {
1432                    let block_arg = interp.try_convert_mut(pattern_bytes.as_slice())?;
1433                    block.yield_arg(interp, &block_arg)?;
1434                }
1435            }
1436            return Ok(value);
1437        }
1438        let matches = string
1439            .find_iter(&pattern_bytes)
1440            .enumerate()
1441            .last()
1442            .map(|(m, _)| m + 1)
1443            .unwrap_or_default();
1444        let mut result = Vec::with_capacity(matches);
1445        for _ in 0..matches {
1446            result.push(interp.try_convert_mut(pattern_bytes.as_slice())?);
1447        }
1448        return interp.try_convert_mut(result);
1449    }
1450    let mut message = String::from("wrong argument type ");
1451    message.push_str(interp.inspect_type_name_for_value(pattern));
1452    message.push_str(" (expected Regexp)");
1453    Err(TypeError::from(message).into())
1454}
1455
1456pub fn setbyte(interp: &mut Artichoke, mut value: Value, index: Value, byte: Value) -> Result<Value, Error> {
1457    let index = implicitly_convert_to_int(interp, index)?;
1458    let i64_byte = implicitly_convert_to_int(interp, byte)?;
1459
1460    check_frozen(interp, value)?;
1461
1462    let mut s = unsafe { super::String::unbox_from_value(&mut value, interp)? };
1463    let index = if let Some(index) = aref::offset_to_index(index, s.len()) {
1464        index
1465    } else {
1466        let mut message = String::from("index ");
1467        // Suppress error because `String`'s `fmt::Write` impl is infallible.
1468        // (It will abort on OOM).
1469        let _ignored = write!(&mut message, "{index} out of string");
1470        return Err(IndexError::from(message).into());
1471    };
1472    // Wrapping when negative is intentional
1473    //
1474    // ```
1475    // [3.0.2] > s = "abc"
1476    // => "abc"
1477    // [3.0.2] > s.setbyte(-3, 99)
1478    // => 99
1479    // [3.0.2] > s
1480    // => "cbc"
1481    // [3.0.2] > s.setbyte(-3, 255)
1482    // => 255
1483    // [3.0.2] > s
1484    // => "\xFFbc"
1485    // [3.0.2] > s.setbyte(-3, 256)
1486    // => 256
1487    // [3.0.2] > s
1488    // => "\u0000bc"
1489    // [3.0.2] > s.setbyte(-3, 257)
1490    // => 257
1491    // [3.0.2] > s
1492    // => "\u0001bc"
1493    // [3.0.2] > s.setbyte(-3, -1)
1494    // => -1
1495    // [3.0.2] > s
1496    // => "\xFFbc"
1497    // ```
1498    let u8_byte = (i64_byte % 256)
1499        .try_into()
1500        .expect("taking mod 256 guarantees the resulting i64 is in range for u8");
1501    // SAFETY: No need to repack, this is an in-place mutation.
1502    unsafe {
1503        let string_mut = s.as_inner_mut();
1504        let cell = string_mut.get_mut(index).ok_or_else(|| {
1505            let mut message = String::from("index ");
1506            // Suppress error because `String`'s `fmt::Write` impl is infallible.
1507            // (It will abort on OOM).
1508            let _ignored = write!(&mut message, "{index} out of string");
1509            IndexError::from(message)
1510        })?;
1511        *cell = u8_byte;
1512    }
1513
1514    // Return the original byte argument.
1515    Ok(byte)
1516}
1517
1518pub fn slice_bang(interp: &mut Artichoke, mut value: Value) -> Result<Value, Error> {
1519    let _s = unsafe { super::String::unbox_from_value(&mut value, interp)? };
1520    Err(NotImplementedError::new().into())
1521}
1522
1523pub fn split(interp: &mut Artichoke, mut value: Value) -> Result<Value, Error> {
1524    let _s = unsafe { super::String::unbox_from_value(&mut value, interp)? };
1525    Err(NotImplementedError::new().into())
1526}
1527
1528pub fn start_with<T>(interp: &mut Artichoke, mut value: Value, prefixes: T) -> Result<Value, Error>
1529where
1530    T: IntoIterator<Item = Value>,
1531{
1532    let s = unsafe { super::String::unbox_from_value(&mut value, interp)? };
1533
1534    for mut prefix in prefixes {
1535        if prefix.ruby_type() == Ruby::String {
1536            let needle = unsafe { super::String::unbox_from_value(&mut prefix, interp)? };
1537            if s.starts_with(needle.as_inner_ref()) {
1538                return Ok(interp.convert(true));
1539            }
1540        } else {
1541            #[cfg(feature = "core-regexp")]
1542            if let Ok(regexp) = unsafe { Regexp::unbox_from_value(&mut prefix, interp) } {
1543                let mut inner = regexp.inner().match_(interp, &s, None, None)?;
1544                if inner.is_nil() {
1545                    continue;
1546                }
1547
1548                let match_data = unsafe { MatchData::unbox_from_value(&mut inner, interp)? };
1549                if match_data.begin(matchdata::Capture::GroupIndex(0))? == Some(0) {
1550                    return Ok(interp.convert(true));
1551                }
1552
1553                regexp::clear_capture_globals(interp)?;
1554                interp.unset_global_variable(regexp::LAST_MATCH)?;
1555                interp.unset_global_variable(regexp::STRING_LEFT_OF_MATCH)?;
1556                interp.unset_global_variable(regexp::STRING_RIGHT_OF_MATCH)?;
1557                continue;
1558            }
1559
1560            // SAFETY: `s` used and discarded immediately before any intervening operations on the VM.
1561            // This ensures there are no intervening garbage collections which may free the `RString*` that backs this value.
1562            let needle = unsafe { implicitly_convert_to_string(interp, &mut prefix)? };
1563            if s.starts_with(needle) {
1564                return Ok(interp.convert(true));
1565            }
1566        }
1567    }
1568
1569    Ok(interp.convert(false))
1570}
1571
1572pub fn swapcase(interp: &mut Artichoke, mut value: Value) -> Result<Value, Error> {
1573    let s = unsafe { super::String::unbox_from_value(&mut value, interp)? };
1574    let mut dup = s.clone();
1575    dup.make_swapcase();
1576    super::String::alloc_value(dup, interp)
1577}
1578
1579pub fn swapcase_bang(interp: &mut Artichoke, mut value: Value) -> Result<Value, Error> {
1580    check_frozen(interp, value)?;
1581
1582    let mut s = unsafe { super::String::unbox_from_value(&mut value, interp)? };
1583    // SAFETY: The string is repacked before any intervening uses of `interp`
1584    // which means no mruby heap allocations can occur.
1585    let (effect, returned) = unsafe {
1586        let string_mut = s.as_inner_mut();
1587        // `make_swapcase` might reallocate the string and invalidate the boxed
1588        // pointer, capacity, length triple.
1589        let effect = string_mut.make_swapcase();
1590
1591        let s = s.take();
1592        (effect, super::String::box_into_value(s, value, interp)?)
1593    };
1594    if effect.changed() {
1595        Ok(returned)
1596    } else {
1597        Ok(Value::nil())
1598    }
1599}
1600
1601pub fn to_f(interp: &mut Artichoke, mut value: Value) -> Result<Value, Error> {
1602    let _s = unsafe { super::String::unbox_from_value(&mut value, interp)? };
1603    Err(NotImplementedError::new().into())
1604}
1605
1606pub fn to_i(interp: &mut Artichoke, mut value: Value, base: Option<Value>) -> Result<Value, Error> {
1607    let s = unsafe { super::String::unbox_from_value(&mut value, interp)? };
1608    let mut slice = s.as_slice();
1609    // ignore preceding whitespace
1610    if let Some(start) = slice.iter().position(|&c| !posix_space::is_space(c)) {
1611        slice = &slice[start..];
1612    } else {
1613        // All whitespace, but we cant return early because we need to ensure the base is valid too
1614        slice = &[];
1615    }
1616
1617    // Grab sign before prefix matching
1618    let sign = match slice.first() {
1619        Some(b'+') => {
1620            slice = &slice[1..];
1621            1
1622        }
1623        Some(b'-') => {
1624            slice = &slice[1..];
1625            -1
1626        }
1627        _ => 1,
1628    };
1629
1630    let base = base.map_or(Ok(10), |b| implicitly_convert_to_int(interp, b))?;
1631    let base = match base {
1632        x if x < 0 || x == 1 || x > 36 => return Err(ArgumentError::from(format!("invalid radix {base}")).into()),
1633        0 => {
1634            // Infer base size from prefix
1635            if slice.len() < 2 || slice[0] != b'0' {
1636                10
1637            } else {
1638                match &slice[1] {
1639                    b'b' | b'B' => {
1640                        slice = &slice[2..];
1641                        2
1642                    }
1643                    b'o' | b'O' => {
1644                        slice = &slice[2..];
1645                        8
1646                    }
1647                    b'd' | b'D' => {
1648                        slice = &slice[2..];
1649                        10
1650                    }
1651                    b'x' | b'X' => {
1652                        slice = &slice[2..];
1653                        16
1654                    }
1655                    _ => {
1656                        // Numbers that start with 0 are assumed to be octal
1657                        slice = &slice[1..];
1658                        8
1659                    }
1660                }
1661            }
1662        }
1663        x => {
1664            // Trim leading literal specifier if it exists
1665            if slice.len() >= 2
1666                && matches!(
1667                    (x, &slice[0..2]),
1668                    (2, b"0b" | b"0B") | (8, b"0o" | b"0O") | (10, b"0d" | b"0D") | (16, b"0x" | b"0X")
1669                )
1670            {
1671                slice = &slice[2..];
1672            };
1673
1674            // This can only be 2-36 inclusive in this branch, so unwrap is safe
1675            u32::try_from(x).unwrap()
1676        }
1677    };
1678
1679    // Check string doesn't start with any special characters:
1680    //
1681    // - '_' is invalid because they are stripped out elsewhere in the string,
1682    //  but cannot start a number.
1683    // - '+' and '-' invalid because we already have the sign prior to the
1684    //   prefix, but they are accepted at the beginning of a string by
1685    //   `str_from_radix`, and double sign doesn't make sense
1686    if matches!(slice.first(), Some(&b'_' | &b'+' | &b'-')) {
1687        return Ok(interp.convert(0));
1688    }
1689
1690    // Double underscores are not valid, and we should stop parsing the string if we encounter one
1691    if let Some(double_underscore) = slice.find("__") {
1692        slice = &slice[..double_underscore];
1693    }
1694
1695    // Single underscores should be ignored
1696    let mut slice = std::borrow::Cow::from(slice);
1697    if slice.find("_").is_some() {
1698        slice.to_mut().retain(|&c| c != b'_');
1699    }
1700    loop {
1701        use std::num::IntErrorKind;
1702        // Try to greedily parse the whole string as an int.
1703        let parsed = str::from_utf8(&slice)
1704            .map_err(|_| IntErrorKind::InvalidDigit)
1705            .and_then(|s| i64::from_str_radix(s, base).map_err(|err| err.kind().clone()));
1706        match parsed {
1707            Ok(int) => return Ok(interp.convert(sign * int)),
1708            Err(IntErrorKind::Empty | IntErrorKind::Zero) => return Ok(interp.convert(0)),
1709            Err(IntErrorKind::PosOverflow | IntErrorKind::NegOverflow) => {
1710                return Err(NotImplementedError::new().into());
1711            }
1712            _ => {
1713                // if parsing failed, start discarding from the end one byte at a time.
1714                match slice {
1715                    std::borrow::Cow::Owned(ref mut data) => {
1716                        data.pop();
1717                    }
1718                    std::borrow::Cow::Borrowed(data) => {
1719                        slice = std::borrow::Cow::from(&data[..(data.len() - 1)]);
1720                    }
1721                }
1722            }
1723        }
1724    }
1725}
1726
1727pub fn to_s(interp: &mut Artichoke, mut value: Value) -> Result<Value, Error> {
1728    let _s = unsafe { super::String::unbox_from_value(&mut value, interp)? };
1729    Ok(value)
1730}
1731
1732pub fn upcase(interp: &mut Artichoke, mut value: Value) -> Result<Value, Error> {
1733    let s = unsafe { super::String::unbox_from_value(&mut value, interp)? };
1734    let mut dup = s.clone();
1735    dup.make_uppercase();
1736    super::String::alloc_value(dup, interp)
1737}
1738
1739pub fn upcase_bang(interp: &mut Artichoke, mut value: Value) -> Result<Value, Error> {
1740    check_frozen(interp, value)?;
1741
1742    let mut s = unsafe { super::String::unbox_from_value(&mut value, interp)? };
1743    // SAFETY: The string is repacked before any intervening uses of `interp`
1744    // which means no mruby heap allocations can occur.
1745    let (effect, returned) = unsafe {
1746        let string_mut = s.as_inner_mut();
1747        // `make_uppercase` might reallocate the string and invalidate the boxed
1748        // pointer, capacity, length triple.
1749        let effect = string_mut.make_uppercase();
1750
1751        let s = s.take();
1752        (effect, super::String::box_into_value(s, value, interp)?)
1753    };
1754    if effect.changed() {
1755        Ok(returned)
1756    } else {
1757        Ok(Value::nil())
1758    }
1759}
1760
1761pub fn is_valid_encoding(interp: &mut Artichoke, mut value: Value) -> Result<Value, Error> {
1762    let s = unsafe { super::String::unbox_from_value(&mut value, interp)? };
1763    Ok(interp.convert(s.is_valid_encoding()))
1764}
1765
1766fn check_frozen(interp: &mut Artichoke, mut value: Value) -> Result<(), Error> {
1767    if !value.is_frozen(interp) {
1768        return Ok(());
1769    }
1770    let s = unsafe { super::String::unbox_from_value(&mut value, interp)? };
1771    let message = "can't modify frozen String: "
1772        .chars()
1773        .chain(s.inspect())
1774        .collect::<super::String>();
1775    Err(FrozenError::from(message.into_vec()).into())
1776}
1777
1778#[cfg(test)]
1779mod tests {
1780    use bstr::ByteSlice;
1781
1782    use super::*;
1783    use crate::test::prelude::*;
1784
1785    #[test]
1786    fn mutating_methods_may_raise_frozen_error() {
1787        #[track_caller]
1788        fn assert_is_frozen_err(result: Result<Value, Error>) {
1789            let err = result.unwrap_err();
1790            let message = err.message();
1791            let class_name = err.name();
1792            assert_eq!(class_name, "FrozenError");
1793            assert_eq!(message.as_bstr(), br#"can't modify frozen String: "foo""#.as_bstr());
1794        }
1795
1796        let mut interp = interpreter();
1797        let mut slf = interp.eval(b"'foo'").unwrap();
1798
1799        slf.freeze(&mut interp).unwrap();
1800
1801        let other = interp.try_convert_mut("bar").unwrap();
1802
1803        assert_is_frozen_err(append(&mut interp, slf, other));
1804        assert_is_frozen_err(aset(&mut interp, slf));
1805        assert_is_frozen_err(capitalize_bang(&mut interp, slf));
1806        assert_is_frozen_err(chomp_bang(&mut interp, slf, None));
1807        assert_is_frozen_err(chop_bang(&mut interp, slf));
1808        assert_is_frozen_err(clear(&mut interp, slf));
1809        assert_is_frozen_err(downcase_bang(&mut interp, slf));
1810        assert_is_frozen_err(initialize_copy(&mut interp, slf, other));
1811        assert_is_frozen_err(reverse_bang(&mut interp, slf));
1812        assert_is_frozen_err(swapcase_bang(&mut interp, slf));
1813        assert_is_frozen_err(upcase_bang(&mut interp, slf));
1814
1815        let index = interp.convert(0);
1816        let byte = interp.convert(0);
1817        assert_is_frozen_err(setbyte(&mut interp, slf, index, byte));
1818    }
1819}