qr_reader-native 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE-mruby.txt +22 -0
  3. data/LICENSE-spinel.txt +19 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +61 -0
  6. data/ext/qr_reader_native/SPINEL_VERSION +1 -0
  7. data/ext/qr_reader_native/extconf.rb +8 -0
  8. data/ext/qr_reader_native/mruby/error.h +13 -0
  9. data/ext/qr_reader_native/mruby/internal.h +145 -0
  10. data/ext/qr_reader_native/mruby/string.h +22 -0
  11. data/ext/qr_reader_native/mruby.h +93 -0
  12. data/ext/qr_reader_native/mruby_shim.h +118 -0
  13. data/ext/qr_reader_native/qr_reader_native.c +13513 -0
  14. data/ext/qr_reader_native/qr_reader_native.h +29 -0
  15. data/ext/qr_reader_native/qr_reader_native_ext.c +598 -0
  16. data/ext/qr_reader_native/re_cased.h +71 -0
  17. data/ext/qr_reader_native/re_compile.c +5784 -0
  18. data/ext/qr_reader_native/re_ctype.h +3540 -0
  19. data/ext/qr_reader_native/re_exec.c +2190 -0
  20. data/ext/qr_reader_native/re_internal.h +671 -0
  21. data/ext/qr_reader_native/re_prop.h +4629 -0
  22. data/ext/qr_reader_native/re_spinel.c +363 -0
  23. data/ext/qr_reader_native/re_spinel.h +22 -0
  24. data/ext/qr_reader_native/re_spinel_utf8.inc +97 -0
  25. data/ext/qr_reader_native/re_utf8.c +156 -0
  26. data/ext/qr_reader_native/sp_alloc.c +1447 -0
  27. data/ext/qr_reader_native/sp_alloc.h +1196 -0
  28. data/ext/qr_reader_native/sp_argf.h +33 -0
  29. data/ext/qr_reader_native/sp_array.c +1052 -0
  30. data/ext/qr_reader_native/sp_array.h +456 -0
  31. data/ext/qr_reader_native/sp_bigint.c +5826 -0
  32. data/ext/qr_reader_native/sp_bigint.h +117 -0
  33. data/ext/qr_reader_native/sp_cold.c +5259 -0
  34. data/ext/qr_reader_native/sp_compat.h +402 -0
  35. data/ext/qr_reader_native/sp_core.c +818 -0
  36. data/ext/qr_reader_native/sp_core.h +56 -0
  37. data/ext/qr_reader_native/sp_crypto.c +877 -0
  38. data/ext/qr_reader_native/sp_crypto.h +169 -0
  39. data/ext/qr_reader_native/sp_dtoa.c +1533 -0
  40. data/ext/qr_reader_native/sp_dtoa.h +29 -0
  41. data/ext/qr_reader_native/sp_encoding_names.h +172 -0
  42. data/ext/qr_reader_native/sp_enum.h +86 -0
  43. data/ext/qr_reader_native/sp_exc.c +1185 -0
  44. data/ext/qr_reader_native/sp_exc.h +186 -0
  45. data/ext/qr_reader_native/sp_exc_ctx.h +56 -0
  46. data/ext/qr_reader_native/sp_fiber.c +889 -0
  47. data/ext/qr_reader_native/sp_fiber.h +162 -0
  48. data/ext/qr_reader_native/sp_fiber_ctx.h +84 -0
  49. data/ext/qr_reader_native/sp_format.c +695 -0
  50. data/ext/qr_reader_native/sp_format.h +83 -0
  51. data/ext/qr_reader_native/sp_gc.c +1793 -0
  52. data/ext/qr_reader_native/sp_gc.h +762 -0
  53. data/ext/qr_reader_native/sp_hash.c +131 -0
  54. data/ext/qr_reader_native/sp_hash.h +131 -0
  55. data/ext/qr_reader_native/sp_inspect.c +215 -0
  56. data/ext/qr_reader_native/sp_inspect.h +128 -0
  57. data/ext/qr_reader_native/sp_io.c +1676 -0
  58. data/ext/qr_reader_native/sp_io.h +272 -0
  59. data/ext/qr_reader_native/sp_iobuffer.c +1014 -0
  60. data/ext/qr_reader_native/sp_iobuffer.h +167 -0
  61. data/ext/qr_reader_native/sp_marshal.c +487 -0
  62. data/ext/qr_reader_native/sp_marshal.h +60 -0
  63. data/ext/qr_reader_native/sp_net.c +1029 -0
  64. data/ext/qr_reader_native/sp_net.h +205 -0
  65. data/ext/qr_reader_native/sp_pack.c +1254 -0
  66. data/ext/qr_reader_native/sp_poly_cold.c +1220 -0
  67. data/ext/qr_reader_native/sp_proc.c +410 -0
  68. data/ext/qr_reader_native/sp_proc.h +327 -0
  69. data/ext/qr_reader_native/sp_process.c +547 -0
  70. data/ext/qr_reader_native/sp_process_status.c +211 -0
  71. data/ext/qr_reader_native/sp_process_status.h +66 -0
  72. data/ext/qr_reader_native/sp_random.c +287 -0
  73. data/ext/qr_reader_native/sp_random.h +52 -0
  74. data/ext/qr_reader_native/sp_range.h +144 -0
  75. data/ext/qr_reader_native/sp_re.c +1379 -0
  76. data/ext/qr_reader_native/sp_re.h +186 -0
  77. data/ext/qr_reader_native/sp_re_names.h +59 -0
  78. data/ext/qr_reader_native/sp_sched.c +4044 -0
  79. data/ext/qr_reader_native/sp_sched.h +295 -0
  80. data/ext/qr_reader_native/sp_slab.c +1507 -0
  81. data/ext/qr_reader_native/sp_str.c +1462 -0
  82. data/ext/qr_reader_native/sp_str.h +306 -0
  83. data/ext/qr_reader_native/sp_str_crypt.c +23 -0
  84. data/ext/qr_reader_native/sp_string.c +92 -0
  85. data/ext/qr_reader_native/sp_string.h +253 -0
  86. data/ext/qr_reader_native/sp_system.c +78 -0
  87. data/ext/qr_reader_native/sp_system.h +18 -0
  88. data/ext/qr_reader_native/sp_time.c +860 -0
  89. data/ext/qr_reader_native/sp_time.h +100 -0
  90. data/ext/qr_reader_native/sp_types.h +292 -0
  91. data/ext/qr_reader_native/spinel_rt.h +19017 -0
  92. data/ext/qr_reader_native/unicase.c +399 -0
  93. data/ext/qr_reader_native/unicase.h +901 -0
  94. data/lib/qr_reader/native.rb +6 -0
  95. metadata +153 -0
@@ -0,0 +1,1462 @@
1
+ /* sp_str.c -- cold String transforms (see sp_str.h).
2
+ *
3
+ * Leaf `const char*` operations moved out of spinel_rt.h so they compile
4
+ * once into libspinel_rt.a instead of into every generated TU. They reach
5
+ * the GC string heap via sp_alloc.h and the typed arrays via sp_array.h;
6
+ * sp_str_crypt is in lib/sp_str_crypt.c; sp_sprintf / sp_raise_* resolve
7
+ * at the final link against the generated TU. */
8
+ #include <string.h>
9
+ #include <stdlib.h>
10
+ #include <stdio.h>
11
+ #include <ctype.h>
12
+ #include <stdint.h>
13
+ #include "sp_str.h"
14
+ extern SP_TLS int sp_re_sub_matched; /* sp_re.h: the bang forms' "a substitution happened" */
15
+ /* sp_re.h: what gsub / sub / scan with a String pattern leave in `$~`, when
16
+ the program reads it (sp_re_track_last) */
17
+ extern int sp_re_track_last;
18
+ void sp_re_clear_last_match(void);
19
+ void sp_re_set_lit_match(const char *str, sp_int beg, sp_int end);
20
+ /* general categories, for what inspect escapes; `spin ext build` vendors
21
+ the runtime flat, lib/regexp/ beside lib/ */
22
+ #if defined(__has_include)
23
+ # if __has_include("regexp/re_prop.h")
24
+ # include "regexp/re_prop.h"
25
+ # else
26
+ # include "re_prop.h"
27
+ # endif
28
+ #else
29
+ # include "regexp/re_prop.h"
30
+ #endif
31
+
32
+ /* Does String#inspect spell codepoint cp as \uXXXX? CRuby escapes what
33
+ rb_enc_isprint refuses: a C1 control (Cc), a line or paragraph separator
34
+ (Zl, Zp), a surrogate (Cs) and an unassigned codepoint (Cn). A format
35
+ character (Cf) and a space (Zs) print as they are. */
36
+ static int sp_cp_inspect_escapes(uint32_t cp) {
37
+ size_t lo = 0, hi = RE_PROP_GC_RUN_COUNT;
38
+ while (hi - lo > 1) {
39
+ size_t mid = (lo + hi) / 2;
40
+ if ((re_prop_gc_runs[mid] >> RE_PROP_GC_BITS) <= cp) lo = mid;
41
+ else hi = mid;
42
+ }
43
+ const char *gc = re_prop_gc_names[re_prop_gc_runs[lo] & ((1u << RE_PROP_GC_BITS) - 1)];
44
+ return !strcmp(gc, "Cc") || !strcmp(gc, "Zl") || !strcmp(gc, "Zp") ||
45
+ !strcmp(gc, "Cs") || !strcmp(gc, "Cn");
46
+ }
47
+
48
+ /* Single-char substring cache (sub_range fast path); per-process, only
49
+ used by the sub_range helpers that live in this file. */
50
+ static char sp_char_cache[256][3];
51
+ static int sp_char_cache_init = 0;
52
+ /* The same table for a BINARY receiver, where the slice has to keep the tag.
53
+ The tag lives in an sp_str_hdr, and the 0xff entries above have none -- so
54
+ these entries carry one, under the 0xfb marker (static storage WITH a
55
+ header). The header also gives a real length, so byte 0 is representable
56
+ here where strlen makes it the empty string above. Built once; never
57
+ allocated, never swept, never mutated in place (0xfb is not in the
58
+ heap-string group any of those paths test for). */
59
+ typedef struct { sp_str_hdr h; char m; char d[2]; } sp_hdr_char_ent;
60
+ /* [0] plain (whatever the default encoding is -- spinel names one), [1] BINARY.
61
+ Two tables rather than one because the tag is the only thing that differs and
62
+ it has to be in the header the entry carries. */
63
+ static sp_hdr_char_ent sp_hdr_char_cache[2][256];
64
+ static int sp_hdr_char_cache_init = 0;
65
+ static const char *sp_hdr_char(unsigned char c, int binary) {
66
+ if (!sp_hdr_char_cache_init) {
67
+ for (int k = 0; k < 2; k++)
68
+ for (int i = 0; i < 256; i++) {
69
+ sp_hdr_char_cache[k][i].h.next = NULL;
70
+ sp_hdr_char_cache[k][i].h.size = k ? SP_STR_SIZE_BINARY : 0u;
71
+ sp_hdr_char_cache[k][i].h.len = 1;
72
+ sp_hdr_char_cache[k][i].h.hash = 0;
73
+ sp_hdr_char_cache[k][i].m = (char)0xfb;
74
+ sp_hdr_char_cache[k][i].d[0] = (char)i;
75
+ sp_hdr_char_cache[k][i].d[1] = 0;
76
+ }
77
+ sp_hdr_char_cache_init = 1;
78
+ }
79
+ return sp_hdr_char_cache[binary ? 1 : 0][c].d;
80
+ }
81
+ /* Integer#chr: a 1-byte string, no allocation. spinel does not let a program
82
+ name an encoding, so nothing but pack and String#b asks for BYTES -- and a
83
+ chr result is not one of those. It stays on the plain table, which is what
84
+ it has always reported; CRuby would say US-ASCII (or ASCII-8BIT above 0x7F),
85
+ a divergence spinel already accepts for want of a third encoding. */
86
+ const char *sp_plain_char(unsigned char c) { return sp_hdr_char(c, 0); }
87
+ const char *sp_bin_char(unsigned char c) { return sp_hdr_char(c, 1); }
88
+
89
+ /* Hex-digit value, used by sp_str_undump's \xNN / \uNNNN unescape. */
90
+ static int _sp_hexval(unsigned char d){return (d<='9')?(d-'0'):(tolower(d)-'a'+10);}
91
+
92
+ /* gsub/sub replacement backslash expansion (defined near sp_str_gsub). */
93
+ static char *sp_str_rep_expand(const char *rep, const char *match, size_t mlen);
94
+
95
+ /* Byte-oriented substring search (NUL-transparent). spinel Strings can carry
96
+ embedded NULs (pack, socket reads), so the search family must use the header
97
+ byte length rather than strstr's C-string scan, which stops at the first NUL
98
+ and diverges from CRuby's byte-based #include?/#index (issue #1778). Returns
99
+ a pointer into `hay`, or NULL. An empty needle matches at the start. */
100
+ static const char *sp_bytestr(const char *hay, size_t hn, const char *need, size_t nn) {
101
+ if (nn == 0) return hay;
102
+ if (nn > hn) return NULL;
103
+ for (size_t i = 0; i + nn <= hn; i++)
104
+ if (hay[i] == need[0] && memcmp(hay + i, need, nn) == 0) return hay + i;
105
+ return NULL;
106
+ }
107
+ int sp_utf8_set_has(const uint32_t*cps,size_t n,uint32_t cp){for(size_t i=0;i<n;i++)if(cps[i]==cp)return 1;return 0;}
108
+ /* Case-insensitive byte comparison over the REAL lengths: stopping at the
109
+ first NUL made two strings differing only after one compare equal (#3471). */
110
+ sp_int sp_str_casecmp(const char*a,const char*b){if(!a)sp_nil_recv("casecmp");if(!b)return 1;
111
+ size_t la=sp_str_byte_len(a),lb=sp_str_byte_len(b),n=la<lb?la:lb;
112
+ for(size_t i=0;i<n;i++){int ca=tolower((unsigned char)a[i]),cb=tolower((unsigned char)b[i]);if(ca!=cb)return ca<cb?-1:1;}
113
+ return la==lb?0:(la<lb?-1:1);}
114
+ /* ASCII-8BIT has no invalid byte sequence -- every byte is a character -- so a
115
+ BINARY string is valid by definition and never walked. On the text side the
116
+ walk is bounded by the byte length rather than by a terminator: a NUL is a
117
+ valid UTF-8 character, and stopping there would call "a\0\xff" valid. */
118
+ sp_bool sp_str_valid_encoding(const char*s){if(!s)sp_nil_recv("valid_encoding?");
119
+ if(sp_str_is_binary(s))return TRUE;
120
+ const unsigned char*p=(const unsigned char*)s;const unsigned char*e=p+sp_str_byte_len(s);while(p<e){unsigned c=*p;if(c<0x80){p++;continue;}int extra;unsigned cp;unsigned min;if((c&0xE0)==0xC0){extra=1;cp=c&0x1F;min=0x80;}
121
+ else if((c&0xF0)==0xE0){extra=2;cp=c&0x0F;min=0x800;}
122
+ else if((c&0xF8)==0xF0){extra=3;cp=c&0x07;min=0x10000;}
123
+ else return FALSE;p++;for(int i=0;i<extra;i++){if(p>=e||(*p&0xC0)!=0x80)return FALSE;cp=(cp<<6)|(*p&0x3F);p++;}if(cp<min)return FALSE;if(cp>=0xD800&&cp<=0xDFFF)return FALSE;if(cp>0x10FFFF)return FALSE;}return TRUE;}
124
+ /* Extract the n-th field (0-based) from s split by sep, without
125
+ allocating a full StrArray. Returns a newly allocated string.
126
+ If the field doesn't exist, returns "". */
127
+ const char*sp_str_field(const char*s,const char*sep,sp_int n){SP_GC_ROOT_STR(s);SP_GC_ROOT_STR(sep);
128
+ size_t sl=strlen(sep);sp_int cur=0;const char*p=s;
129
+ if(sl==0)return sp_str_empty;
130
+ while(cur<n){const char*f=strstr(p,sep);if(!f)return sp_str_empty;p=f+sl;cur++;}
131
+ const char*end=strstr(p,sep);size_t len=end?((size_t)(end-p)):strlen(p);
132
+ char*r=sp_str_alloc_raw(len+1);memcpy(r,p,len);r[len]=0;sp_str_set_len(r,len);return r;}
133
+ /* Count fields in s split by sep (without allocating). */
134
+ sp_int sp_str_field_count(const char*s,const char*sep){
135
+ if(*s==0)return 0;
136
+ size_t sl=strlen(sep);if(sl==0)return(sp_int)strlen(s);
137
+ sp_int c=1;const char*p=s;while((p=strstr(p,sep))!=NULL){c++;p+=sl;}return c;}
138
+ /* The BINARY tag survives every operation that cannot change what the bytes
139
+ are: a concatenation, a join, a repeat, a slice. CRuby's rule is that an
140
+ ASCII-only operand adopts the other operand's encoding, so a result with any
141
+ binary input is binary -- "hdr" + digest is ASCII-8BIT there, and a slice of
142
+ a binary string is binary because a slice of bytes is bytes.
143
+
144
+ Where CRuby would instead raise Encoding::CompatibilityError (two operands
145
+ that BOTH carry bytes >= 0x80 in different encodings), spinel answers binary
146
+ rather than raising; that is the existing posture of a runtime with two
147
+ encodings, and answering text there was the bug -- an untagged result does
148
+ not compare equal to the same bytes from pack, File.binread or a socket.
149
+
150
+ Only freshly allocated results are marked. The shared empty string and other
151
+ static returns are never touched: marking one would tag it for every other
152
+ holder of the same pointer. */
153
+ static inline char *sp_str_bin_from(char *r, const char *a) {
154
+ if (a && sp_str_is_binary(a)) sp_str_mark_binary(r);
155
+ return r;
156
+ }
157
+
158
+ /* One step of CRuby's encoding rule over a string built part by part (an
159
+ interpolation, Array#join): `state` is the encoding so far, -1 while only
160
+ US-ASCII parts (an Integer's to_s, nothing yet) have gone in, 0 UTF-8, 1
161
+ binary; `acc` holds the `acc_len` bytes already joined. A part in the same
162
+ encoding, or an ASCII-only one, keeps it; one whose high bytes meet an
163
+ ASCII-only run so far takes the run over; two runs with high bytes in
164
+ different encodings stay binary (see above). */
165
+ int sp_str_enc_step(int state, const char *acc, size_t acc_len, const char *part, size_t part_len, int part_bin) {
166
+ if (state < 0) return part_len ? part_bin : -1;
167
+ if (part_bin == state || part_len == 0) return state;
168
+ for (size_t i = 0; i < part_len; i++)
169
+ if ((unsigned char)part[i] >= 0x80) {
170
+ for (size_t j = 0; j < acc_len; j++) if ((unsigned char)acc[j] >= 0x80) return 1;
171
+ return part_bin;
172
+ }
173
+ return state;
174
+ }
175
+ /* Whether a + b is binary, as CRuby's compatibility rule picks it: the
176
+ operands' shared encoding, else the one an ASCII-only operand gives way
177
+ to -- `String.new + "\u00e9"` is UTF-8 -- else binary (see above). */
178
+ static int sp_str_cat_binary(const char *a, const char *b) {
179
+ int ab = sp_str_is_binary(a), bb = sp_str_is_binary(b);
180
+ if (ab == bb) return ab;
181
+ if (sp_str_ascii_only(b)) return ab;
182
+ if (sp_str_ascii_only(a)) return bb;
183
+ return 1;
184
+ }
185
+ /* The same rule folded over a run of parts: `bin` is the encoding of the
186
+ `pl` bytes of r already joined, s the next part. The prefix is scanned only
187
+ when the two encodings differ. */
188
+ static int sp_str_bin_fold(const char *r, size_t pl, int bin, const char *s) {
189
+ int sb = sp_str_is_binary(s);
190
+ if (sb == bin || sp_str_ascii_only(s)) return bin;
191
+ for (size_t i = 0; i < pl; i++) if ((unsigned char)r[i] >= 0x80) return 1;
192
+ return sb;
193
+ }
194
+ const char*sp_str_concat(const char*a,const char*b){SP_GC_ROOT_STR(a);SP_GC_ROOT_STR(b);if(!a)a=sp_str_empty;if(!b)b=sp_str_empty;size_t la=sp_str_byte_len(a),lb=sp_str_byte_len(b);char*r=sp_str_alloc(la+lb);memcpy(r,a,la);memcpy(r+la,b,lb);if(sp_str_cat_binary(a,b))sp_str_mark_binary(r);return r;}
195
+ /* append_as_bytes keeps the receiver's encoding, regardless of the bytes or
196
+ encoding of the appended String. */
197
+ const char*sp_str_append_bytes(const char*a,const char*b){SP_GC_ROOT_STR(a);SP_GC_ROOT_STR(b);if(!a)a=sp_str_empty;if(!b)b=sp_str_empty;size_t la=sp_str_byte_len(a),lb=sp_str_byte_len(b);char*r=sp_str_alloc(la+lb);memcpy(r,a,la);memcpy(r+la,b,lb);return sp_str_bin_from(r,a);}
198
+ /* Issue #760: NULL src to memcpy is UB. Treat NULL as empty string. */
199
+ const char*sp_str_concat3(const char*a,const char*b,const char*c){SP_GC_ROOT_STR(a);SP_GC_ROOT_STR(b);SP_GC_ROOT_STR(c);if(!a)a=sp_str_empty;if(!b)b=sp_str_empty;if(!c)c=sp_str_empty;size_t la=sp_str_byte_len(a),lb=sp_str_byte_len(b),lc=sp_str_byte_len(c);char*r=sp_str_alloc(la+lb+lc);memcpy(r,a,la);memcpy(r+la,b,lb);memcpy(r+la+lb,c,lc);{int ab=sp_str_cat_binary(a,b);int rb=ab==sp_str_is_binary(c)?ab:sp_str_ascii_only(c)?ab:(sp_str_ascii_only(a)&&sp_str_ascii_only(b))?sp_str_is_binary(c):1;if(rb)sp_str_mark_binary(r);}return r;}
200
+ const char*sp_str_concat4(const char*a,const char*b,const char*c,const char*d){SP_GC_ROOT_STR(a);SP_GC_ROOT_STR(b);SP_GC_ROOT_STR(c);SP_GC_ROOT_STR(d);if(!a)a=sp_str_empty;if(!b)b=sp_str_empty;if(!c)c=sp_str_empty;if(!d)d=sp_str_empty;size_t la=sp_str_byte_len(a),lb=sp_str_byte_len(b),lc=sp_str_byte_len(c),ld=sp_str_byte_len(d);char*r=sp_str_alloc(la+lb+lc+ld);memcpy(r,a,la);memcpy(r+la,b,lb);memcpy(r+la+lb,c,lc);memcpy(r+la+lb+lc,d,ld);{int rb=sp_str_is_binary(a);rb=sp_str_bin_fold(r,la,rb,b);rb=sp_str_bin_fold(r,la+lb,rb,c);rb=sp_str_bin_fold(r,la+lb+lc,rb,d);if(rb)sp_str_mark_binary(r);}return r;}
201
+ /* Concatenate N strings into a single GC-managed buffer. */
202
+ /* Issue #760: NULL entries treated as empty strings. */
203
+ const char*sp_str_concat_arr(const char *const *parts,int n){size_t total=0;for(int i=0;i<n;i++)total+=sp_str_byte_len(parts[i]?parts[i]:sp_str_empty);char*r=sp_str_alloc(total);char*p=r;int rb=-1;for(int i=0;i<n;i++){const char*s=parts[i]?parts[i]:sp_str_empty;size_t sl=sp_str_byte_len(s);rb=rb<0?sp_str_is_binary(s):sp_str_bin_fold(r,(size_t)(p-r),rb,s);memcpy(p,s,sl);p+=sl;}if(rb>0)sp_str_mark_binary(r);return r;}
204
+ /* The unresolved-call gate's raise. Deliberately NOT declared noreturn
205
+ (spinel_rt.h): gate arms sit inside hot dispatch functions and a noreturn
206
+ call restructures their CFG; as a plain value-returning extern call the
207
+ arm keeps the sp_box_nil() shape it replaced. sp_raise_cls longjmps, so
208
+ the return never executes. */
209
+ sp_RbVal sp_raise_nomethod(const char *msg) {SP_GC_ROOT_STR(msg);
210
+ sp_raise_cls("NoMethodError", msg);
211
+ return sp_box_nil();
212
+ }
213
+ /* NoMethodError for a String method reaching a nil (NULL) receiver,
214
+ matching CRuby's "undefined method 'upcase' for nil" message shape. */
215
+ void sp_nil_recv(const char*meth){SP_GC_ROOT_STR(meth);
216
+ size_t cap=strlen(meth)+32;
217
+ char*msg=sp_str_alloc_raw(cap);
218
+ snprintf(msg,cap,"undefined method '%s' for nil",meth);
219
+ SP_GC_ROOT_STR(msg);
220
+ sp_raise_cls("NoMethodError",msg);
221
+ }
222
+ sp_int sp_str_length_m(const char*s){if(!s)sp_nil_recv("length");return sp_str_length(s);}
223
+ sp_int sp_str_bytesize_m(const char*s){if(!s)sp_nil_recv("bytesize");return (sp_int)sp_str_byte_len(s);}
224
+ /* A NUL is a byte, not a terminator: "\0abc" is four bytes long and not
225
+ empty, and a binary string starting with one is ordinary. The first-byte
226
+ test still decides every non-empty string in one load; only a leading NUL
227
+ pays for the header read that knows the real length. */
228
+ sp_bool sp_str_empty_p(const char*s){if(!s)sp_nil_recv("empty?");return *s==0&&sp_str_byte_len(s)==0;}
229
+ const char*sp_str_plus(const char*a,const char*b){SP_GC_ROOT_STR(a);SP_GC_ROOT_STR(b);
230
+ if(!a)sp_nil_recv("+");
231
+ if(!b)sp_raise_cls("TypeError","no implicit conversion of nil into String");
232
+ return sp_str_concat(a,b);
233
+ }
234
+ /* `str + "lit"`: the codegen knows the literal's byte length, and a source
235
+ literal is never BINARY, so this skips the literal's header decode and
236
+ encoding check, the nil-to-empty fixups, and the second call frame
237
+ sp_str_plus spends before sp_str_concat. */
238
+ const char *sp_str_plus_lit(const char *a, const char *lit, size_t ll) {
239
+ if (!a) sp_nil_recv("+");
240
+ SP_GC_ROOT_STR(a);
241
+ size_t la = sp_str_byte_len(a);
242
+ char *r = sp_str_alloc(la + ll);
243
+ memcpy(r, a, la);
244
+ memcpy(r + la, lit, ll);
245
+ /* a binary receiver keeps its encoding unless it is ASCII-only and the
246
+ literal is not: then the result is the literal's UTF-8, as CRuby picks */
247
+ if (sp_str_is_binary(a)) {
248
+ int lit_ascii = 1;
249
+ for (size_t i = 0; i < ll && lit_ascii; i++) lit_ascii = (unsigned char)lit[i] < 0x80;
250
+ if (lit_ascii || !sp_str_ascii_only(a)) sp_str_mark_binary(r);
251
+ }
252
+ return r;
253
+ }
254
+ /* FrozenError naming the receiver, matching CRuby's
255
+ "can't modify frozen String: \"abc\"" message shape. */
256
+ void sp_exc_stage_recv(sp_RbVal v); /* generated TU: stages FrozenError#receiver */
257
+ void sp_raise_frozen_str(const char*s){SP_GC_ROOT_STR(s);const char*ins=sp_str_inspect(s);SP_GC_ROOT_STR(ins);const char*msg=sp_str_concat(&("\xff" "can't modify frozen String: ")[1],ins);SP_GC_ROOT_STR(msg);sp_exc_stage_recv(sp_box_str(s));sp_raise_cls("FrozenError",msg);}
258
+ /* String#inspect: wrap in double quotes and escape \, ", \n, \t, \r,
259
+ plus any non-printable byte as \xNN. Output is always ASCII-safe. */
260
+ const char*sp_str_inspect(const char*s){SP_GC_ROOT_STR(s);if(!s){char*r=sp_str_alloc_raw(4);r[0]='n';r[1]='i';r[2]='l';r[3]=0;return r;}size_t sl=sp_str_byte_len(s);size_t cap=(sl*6)+3;char*r=sp_str_alloc_raw(cap);size_t o=0;r[o++]='"';for(size_t i=0;i<sl;i++){unsigned char c=(unsigned char)s[i];if(c=='\\'||c=='"'){r[o++]='\\';r[o++]=c;}
261
+ /* `#` is escaped only where it would start an interpolation (#3558) */
262
+ else if(c=='#'&&i+1<sl&&(s[i+1]=='{'||s[i+1]=='$'||s[i+1]=='@')){r[o++]='\\';r[o++]='#';}
263
+ else if(c=='\a'){r[o++]='\\';r[o++]='a';}
264
+ else if(c=='\b'){r[o++]='\\';r[o++]='b';}
265
+ else if(c=='\t'){r[o++]='\\';r[o++]='t';}
266
+ else if(c=='\n'){r[o++]='\\';r[o++]='n';}
267
+ else if(c=='\v'){r[o++]='\\';r[o++]='v';}
268
+ else if(c=='\f'){r[o++]='\\';r[o++]='f';}
269
+ else if(c=='\r'){r[o++]='\\';r[o++]='r';}
270
+ else if(c==0x1b){r[o++]='\\';r[o++]='e';}
271
+ else if(c<0x20||c==0x7f){/* other control bytes render as \uNNNN (UTF-8 default, matching CRuby source-literal strings); a BINARY string -- what pack and Random#bytes answer -- renders \xNN as CRuby does for ASCII-8BIT (#3553) */if(sp_str_is_binary(s)){snprintf(r+o,5,"\\x%02X",c);o+=4;}else{snprintf(r+o,7,"\\u%04X",c);o+=6;}}
272
+ /* A byte that is not part of a valid UTF-8 sequence escapes as \xNN, the way
273
+ CRuby renders it: `"a\x80b".inspect` is "a\x80b". Passing the raw byte
274
+ through made the inspect output itself invalid UTF-8, so a terminal drew
275
+ U+FFFD -- inspect lost the one thing it was being asked about. A BINARY
276
+ string escapes every high byte, as the control-byte arm above already does
277
+ for it. A valid sequence is copied through unchanged. */
278
+ else if(c>=0x80){
279
+ int extra=(c&0xE0)==0xC0?1:(c&0xF0)==0xE0?2:(c&0xF8)==0xF0?3:-1;
280
+ int ok=extra>0&&!sp_str_is_binary(s)&&i+(size_t)extra<sl;
281
+ for(int k=1;ok&&k<=extra;k++)if(((unsigned char)s[i+(size_t)k]&0xC0)!=0x80)ok=0;
282
+ if(!ok){snprintf(r+o,5,"\\x%02X",c);o+=4;}
283
+ else{
284
+ /* a valid character that does not print is spelled by its codepoint,
285
+ as CRuby does: "\u0080", "\u2028", "\u{E0080}" */
286
+ uint32_t cp=(uint32_t)(c&(extra==1?0x1F:extra==2?0x0F:0x07));
287
+ for(int k=1;k<=extra;k++)cp=(cp<<6)|((unsigned char)s[i+(size_t)k]&0x3F);
288
+ if(sp_cp_inspect_escapes(cp)){
289
+ if(cp>0xFFFF){int w=snprintf(r+o,12,"\\u{%X}",(unsigned)cp);o+=(size_t)w;}
290
+ else{snprintf(r+o,7,"\\u%04X",(unsigned)cp);o+=6;}
291
+ }
292
+ else for(int k=0;k<=extra;k++)r[o++]=s[i+(size_t)k];
293
+ i+=(size_t)extra;}
294
+ }
295
+ else{r[o++]=(char)c;}}r[o++]='"';r[o]=0;sp_str_set_len(r,o);return r;}
296
+ /* A symbol prints without quotes when its name is a plain identifier (an
297
+ @ivar / @@cvar / $gvar, or a bare name optionally ending in ? ! =) or a
298
+ known operator method name; otherwise it is quoted like a string: :"a b". */
299
+ sp_bool sp_sym_plain_name_p(const char *p, sp_bool allow_suffix) {
300
+ /* Identifier classification, locale-independent on purpose (no LC_CTYPE):
301
+ ASCII letters/digits/underscore by table, and any byte >= 0x80 counts as
302
+ an identifier character -- CRuby treats non-ASCII codepoints as valid
303
+ unquoted symbol characters (`:café` inspects without quotes). */
304
+ unsigned char c = (unsigned char)*p;
305
+ if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' || c >= 0x80)) return FALSE;
306
+ for (p++; (c = (unsigned char)*p) != '\0'; p++)
307
+ if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
308
+ (c >= '0' && c <= '9') || c == '_' || c >= 0x80)) break;
309
+ if (allow_suffix && (*p == '?' || *p == '!' || *p == '=')) p++;
310
+ return *p == '\0';
311
+ }
312
+ sp_bool sp_sym_simple_p(const char *n) {SP_GC_ROOT_STR(n);
313
+ if (!n || !*n) return FALSE;
314
+ /* A name holding a NUL is never a plain identifier, and the walks below all
315
+ stop at it -- `:"a\0b"` would read as the plain name "a" and print bare
316
+ (#nul symbols). Quote it, and sp_str_inspect spells the byte. */
317
+ if (sp_str_byte_len(n) != strlen(n)) return FALSE;
318
+ if (*n == '$') return sp_sym_plain_name_p(n + 1, FALSE);
319
+ if (*n == '@') { const char *p = n + 1; if (*p == '@') p++; return sp_sym_plain_name_p(p, FALSE); }
320
+ if (sp_sym_plain_name_p(n, TRUE)) return TRUE;
321
+ static const char *const ops[] = {
322
+ "+", "-", "*", "/", "%", "**", "==", "===", "!=", "=~", "!~",
323
+ "<", "<=", ">", ">=", "<=>", "<<", ">>", "&", "|", "^", "~",
324
+ "!", "+@", "-@", "[]", "[]=", "`", NULL };
325
+ for (int i = 0; ops[i]; i++) if (!strcmp(n, ops[i])) return TRUE;
326
+ return FALSE;
327
+ }
328
+ const char *sp_sym_inspect_name(const char *name) {SP_GC_ROOT_STR(name);
329
+ /* Build ":" + body directly rather than sp_str_concat(":", body): a bare
330
+ literal like ":" has no length-marker byte, so sp_str_byte_len would read
331
+ one byte before it (out of bounds of the .rodata constant). `body` is a
332
+ real spinel string (the symbol's name, or its inspected form), so measuring
333
+ it is safe. */
334
+ const char *body = sp_sym_simple_p(name) ? name : sp_str_inspect(name);
335
+ SP_GC_ROOT_STR(body); /* the non-simple branch just allocated body; keep it live across sp_str_alloc's GC */
336
+ size_t bl = sp_str_byte_len(body);
337
+ char *r = sp_str_alloc(1 + bl);
338
+ r[0] = ':';
339
+ memcpy(r + 1, body, bl);
340
+ return r;
341
+ }
342
+ /* A symbol hash key in the `key: value` short form: a simple name is bare, a
343
+ name needing quotes is string-quoted (`"k space": ...`) -- no leading colon. */
344
+ const char *sp_sym_inspect_key(const char *name) {SP_GC_ROOT_STR(name);
345
+ return sp_sym_simple_p(name) ? name : sp_str_inspect(name);
346
+ }
347
+ /* Simple Unicode case mapping for ASCII + Latin-1 Supplement (U+00C0-00FF)
348
+ + Latin Extended-A (U+0100-017F), the ranges the ruby/spec string case
349
+ specs exercise. Full Unicode is out of scope (one internal UTF-8 rep). */
350
+ uint32_t sp_uc_toupper(uint32_t cp){
351
+ if(cp<0x80)return (uint32_t)toupper((int)cp);
352
+ if(cp>=0xE0&&cp<=0xFE&&cp!=0xF7)return cp-0x20; /* Latin-1 lower -> upper */
353
+ if(cp==0xFF)return 0x178; /* ÿ -> Ÿ */
354
+ if(cp>=0x100&&cp<=0x137){ if(cp&1)return cp-1; return cp; } /* Latin-Ext-A pairs */
355
+ if(cp>=0x139&&cp<=0x148){ if((cp&1)==0)return cp-1; return cp; }
356
+ if(cp>=0x14A&&cp<=0x177){ if(cp&1)return cp-1; return cp; }
357
+ if(cp>=0x179&&cp<=0x17E){ if((cp&1)==0)return cp-1; return cp; }
358
+ return cp;
359
+ }
360
+ uint32_t sp_uc_tolower(uint32_t cp){
361
+ if(cp<0x80)return (uint32_t)tolower((int)cp);
362
+ if(cp>=0xC0&&cp<=0xDE&&cp!=0xD7)return cp+0x20; /* Latin-1 upper -> lower */
363
+ if(cp==0x178)return 0xFF; /* Ÿ -> ÿ */
364
+ if(cp>=0x100&&cp<=0x137){ if((cp&1)==0)return cp+1; return cp; }
365
+ if(cp>=0x139&&cp<=0x148){ if(cp&1)return cp+1; return cp; }
366
+ if(cp>=0x14A&&cp<=0x177){ if((cp&1)==0)return cp+1; return cp; }
367
+ if(cp>=0x179&&cp<=0x17E){ if(cp&1)return cp+1; return cp; }
368
+ return cp;
369
+ }
370
+ /* Map every codepoint of s through fn; ß (U+00DF) upcases to "SS" when
371
+ up is set, so allocate room for a 3x expansion. */
372
+ /* byte_len, not strlen: an embedded NUL is a byte of the string, and mapping
373
+ only up to it silently truncated the result (the opportunistic NUL policy --
374
+ fix what is met). U+0000 maps to itself and encodes as the one byte. */
375
+ static const char*sp_str_case_map(const char*s,uint32_t(*fn)(uint32_t),int up){SP_GC_ROOT_STR(s);
376
+ size_t l=sp_str_byte_len(s);char*r=sp_str_alloc_raw(l*3+1);size_t oi=0;
377
+ for(size_t i=0;i<l;){uint32_t cp;int n=sp_utf8_decode(s+i,&cp);i+=(size_t)n;
378
+ if(up&&cp==0xDF){r[oi++]='S';r[oi++]='S';continue;}
379
+ oi+=(size_t)sp_utf8_encode(fn(cp),r+oi);}
380
+ r[oi]=0;sp_str_set_len(r,oi);return r;
381
+ }
382
+ /* casecmp? compares the Unicode case FOLDING of both sides, not bytes under
383
+ ASCII tolower: "\u00c9".casecmp?("\u00e9") is true. Folding here is the
384
+ lowercase mapping above over the same ranges, plus the few sources in them
385
+ whose folding is not their lowercase: sharp s (and capital sharp s) fold to
386
+ "ss", long s to "s", micro sign to Greek mu, and dotted capital I to "i"
387
+ and a combining dot. */
388
+ static size_t sp_uc_fold_into(uint32_t cp,char*o){
389
+ if(cp==0xDF||cp==0x1E9E){o[0]='s';o[1]='s';return 2;}
390
+ if(cp==0x17F){o[0]='s';return 1;}
391
+ if(cp==0xB5)return (size_t)sp_utf8_encode(0x3BC,o);
392
+ if(cp==0x130){o[0]='i';return 1+(size_t)sp_utf8_encode(0x307,o+1);}
393
+ return (size_t)sp_utf8_encode(sp_uc_tolower(cp),o);
394
+ }
395
+ static char*sp_str_fold_buf(const char*s,size_t*out){
396
+ size_t l=sp_str_byte_len(s);char*r=(char*)malloc(l*3+1);if(!r){perror("malloc");exit(1);}size_t oi=0;
397
+ for(size_t i=0;i<l;){uint32_t cp;int n=sp_utf8_decode(s+i,&cp);i+=(size_t)n;oi+=sp_uc_fold_into(cp,r+oi);}
398
+ *out=oi;return r;
399
+ }
400
+ sp_bool sp_str_casecmp_p(const char*a,const char*b){if(!a)sp_nil_recv("casecmp?");if(!b)return FALSE;
401
+ size_t la=sp_str_byte_len(a),lb=sp_str_byte_len(b);int ascii=1;
402
+ for(size_t i=0;i<la&&ascii;i++)if((unsigned char)a[i]>=0x80)ascii=0;
403
+ for(size_t i=0;i<lb&&ascii;i++)if((unsigned char)b[i]>=0x80)ascii=0;
404
+ if(ascii)return sp_str_casecmp(a,b)==0;
405
+ size_t fa,fb;char*x=sp_str_fold_buf(a,&fa);char*y=sp_str_fold_buf(b,&fb);
406
+ sp_bool eq=fa==fb&&memcmp(x,y,fa)==0;free(x);free(y);return eq;
407
+ }
408
+ const char*sp_str_upcase(const char*s){SP_GC_ROOT_STR(s);if(!s)sp_nil_recv("upcase");return sp_str_case_map(s,sp_uc_toupper,1);}
409
+ const char*sp_str_downcase(const char*s){SP_GC_ROOT_STR(s);if(!s)sp_nil_recv("downcase");return sp_str_case_map(s,sp_uc_tolower,0);}
410
+ const char*sp_str_swapcase(const char*s){SP_GC_ROOT_STR(s);if(!s)sp_nil_recv("swapcase");size_t l=sp_str_byte_len(s);char*r=sp_str_alloc_raw(l*3+1);size_t oi=0;for(size_t i=0;i<l;){uint32_t cp;int n=sp_utf8_decode(s+i,&cp);i+=(size_t)n;uint32_t up=sp_uc_toupper(cp),lo=sp_uc_tolower(cp);if(up!=cp){/* cp is lowercase -> uppercase */if(cp==0xDF){r[oi++]='S';r[oi++]='S';}
411
+ else oi+=(size_t)sp_utf8_encode(up,r+oi);}
412
+ else if(lo!=cp){/* cp is uppercase -> lowercase */oi+=(size_t)sp_utf8_encode(lo,r+oi);}
413
+ else oi+=(size_t)sp_utf8_encode(cp,r+oi);}r[oi]=0;sp_str_set_len(r,oi);return r;}
414
+ /* The `:ascii` option (upcase(:ascii)) restricts folding to A-Z/a-z and leaves
415
+ every non-ASCII byte untouched, so these copy byte-for-byte. */
416
+ const char*sp_str_upcase_ascii(const char*s){SP_GC_ROOT_STR(s);if(!s)sp_nil_recv("upcase");size_t l=strlen(s);char*r=sp_str_alloc_raw(l+1);for(size_t i=0;i<l;i++){unsigned char ch=(unsigned char)s[i];r[i]=(ch>='a'&&ch<='z')?(char)(ch-32):(char)ch;}r[l]=0;sp_str_set_len(r,l);return r;}
417
+ const char*sp_str_downcase_ascii(const char*s){SP_GC_ROOT_STR(s);if(!s)sp_nil_recv("downcase");size_t l=strlen(s);char*r=sp_str_alloc_raw(l+1);for(size_t i=0;i<l;i++){unsigned char ch=(unsigned char)s[i];r[i]=(ch>='A'&&ch<='Z')?(char)(ch+32):(char)ch;}r[l]=0;sp_str_set_len(r,l);return r;}
418
+ const char*sp_str_swapcase_ascii(const char*s){SP_GC_ROOT_STR(s);if(!s)sp_nil_recv("swapcase");size_t l=strlen(s);char*r=sp_str_alloc_raw(l+1);for(size_t i=0;i<l;i++){unsigned char ch=(unsigned char)s[i];if(ch>='a'&&ch<='z')r[i]=(char)(ch-32);else if(ch>='A'&&ch<='Z')r[i]=(char)(ch+32);else r[i]=(char)ch;}r[l]=0;sp_str_set_len(r,l);return r;}
419
+ const char*sp_str_capitalize_ascii(const char*s){SP_GC_ROOT_STR(s);if(!s)sp_nil_recv("capitalize");size_t l=strlen(s);char*r=sp_str_alloc_raw(l+1);for(size_t i=0;i<l;i++){unsigned char ch=(unsigned char)s[i];if(i==0)r[i]=(ch>='a'&&ch<='z')?(char)(ch-32):(char)ch;else r[i]=(ch>='A'&&ch<='Z')?(char)(ch+32):(char)ch;}r[l]=0;sp_str_set_len(r,l);return r;}
420
+ /* String#dump: a double-quoted, escaped form that sp_str_undump reverses.
421
+ UTF-8 high bytes pass through literally (undump copies them back), so a
422
+ dump/undump round-trip is byte-identical. */
423
+ const char*sp_str_dump(const char*s){SP_GC_ROOT_STR(s);
424
+ if(!s)sp_nil_recv("dump");
425
+ size_t n=sp_str_byte_len(s); /* a NUL is a byte to escape, not the end */
426
+ char*out=sp_str_alloc_raw((n*4)+3);size_t oi=0;
427
+ out[oi++]='"';
428
+ for(size_t i=0;i<n;i++){
429
+ unsigned char c=(unsigned char)s[i];
430
+ if(c=='"'){out[oi++]='\\';out[oi++]='"';}
431
+ else if(c=='\\'){out[oi++]='\\';out[oi++]='\\';}
432
+ /* `#` is escaped only where it would start an interpolation (#3558) */
433
+ else if(c=='#'&&i+1<n&&(s[i+1]=='{'||s[i+1]=='$'||s[i+1]=='@')){out[oi++]='\\';out[oi++]='#';}
434
+ else if(c=='\n'){out[oi++]='\\';out[oi++]='n';}
435
+ else if(c=='\t'){out[oi++]='\\';out[oi++]='t';}
436
+ else if(c=='\r'){out[oi++]='\\';out[oi++]='r';}
437
+ else if(c=='\f'){out[oi++]='\\';out[oi++]='f';}
438
+ else if(c=='\v'){out[oi++]='\\';out[oi++]='v';}
439
+ else if(c=='\a'){out[oi++]='\\';out[oi++]='a';}
440
+ else if(c=='\b'){out[oi++]='\\';out[oi++]='b';}
441
+ else if(c==27){out[oi++]='\\';out[oi++]='e';}
442
+ /* CRuby dumps a NUL as \x00, never \0: the short form would run into a
443
+ following digit (`"\0" "1"` dumps as "\x001", not "\01") */
444
+ else if(c==0){out[oi++]='\\';out[oi++]='x';out[oi++]='0';out[oi++]='0';}
445
+ else if(c<0x20){oi+=(size_t)sprintf(out+oi,"\\x%02X",c);}
446
+ /* #dump promises a pure-ASCII, re-evaluable form: a non-ASCII character
447
+ is written as its \uXXXX escape (#3558) */
448
+ else if(c>=0x80){
449
+ unsigned cp=0;int extra=0;
450
+ if((c&0xE0)==0xC0){cp=c&0x1Fu;extra=1;}
451
+ else if((c&0xF0)==0xE0){cp=c&0x0Fu;extra=2;}
452
+ else if((c&0xF8)==0xF0){cp=c&0x07u;extra=3;}
453
+ else{oi+=(size_t)sprintf(out+oi,"\\x%02X",c);continue;}
454
+ if(i+(size_t)extra>=n){oi+=(size_t)sprintf(out+oi,"\\x%02X",c);continue;}
455
+ for(int k=0;k<extra;k++)cp=(cp<<6)|((unsigned char)s[++i]&0x3Fu);
456
+ if(cp>0xFFFFu)oi+=(size_t)sprintf(out+oi,"\\u{%X}",cp);
457
+ else oi+=(size_t)sprintf(out+oi,"\\u%04X",cp);
458
+ }
459
+ else{out[oi++]=(char)c;}
460
+ }
461
+ out[oi++]='"';out[oi]=0;sp_str_set_len(out,oi);return out;
462
+ }
463
+ const char*sp_str_delete_prefix(const char*s,const char*p){SP_GC_ROOT_STR(s);SP_GC_ROOT_STR(p);if(!s)sp_nil_recv("delete_prefix");if(!p)return s;size_t sl=strlen(s),pl=strlen(p);if(pl<=sl&&memcmp(s,p,pl)==0){char*r=sp_str_alloc_raw(sl-pl+1);memcpy(r,s+pl,sl-pl+1);sp_str_set_len(r,sl-pl);return r;}char*r=sp_str_alloc_raw(sl+1);memcpy(r,s,sl+1);sp_str_set_len(r,sl);return r;}
464
+ /* `s << x`: append in place when the buffer has room, else move to a buffer
465
+ with room to spare. The emitted form used to be `s = concat(s, x)`, a fresh
466
+ exact-sized copy per append, so building a document one piece at a time was
467
+ quadratic (an 8MB log took minutes). Only a heap string (0xfe/0xfc) is
468
+ written in place; a literal or a frozen string is copied, and the caller has
469
+ already raised on a frozen receiver. */
470
+ const char *sp_str_append_grow(const char *s, const char *t) {SP_GC_ROOT_STR(s);SP_GC_ROOT_STR(t);
471
+ if (!s) return t ? t : sp_str_empty;
472
+ if (!t) return s;
473
+ size_t la = sp_str_byte_len(s), lb = sp_str_byte_len(t);
474
+ if (lb == 0) return s;
475
+ int text_append = sp_str_is_binary(s) && !sp_str_is_binary(t) &&
476
+ !sp_str_ascii_only(t) && sp_str_ascii_only(s);
477
+ unsigned char m = ((const unsigned char *)s)[-1];
478
+ if (m == 0xfe || m == 0xfc) {
479
+ sp_str_hdr *h = ((sp_str_hdr *)(s - 1)) - 1;
480
+ size_t total = (size_t)(h->size & SP_STR_SIZE_MASK);
481
+ size_t cap = total > sizeof(sp_str_hdr) + 2 ? total - sizeof(sp_str_hdr) - 2 : 0;
482
+ if (la + lb <= cap) {
483
+ memcpy((char *)s + la, t, lb);
484
+ ((char *)s)[la + lb] = 0;
485
+ sp_str_lcache_drop(s);
486
+ sp_str_set_len((char *)s, la + lb);
487
+ if (text_append) sp_str_as_text((char *)s);
488
+ return s;
489
+ }
490
+ }
491
+ size_t want = (la + lb) * 2 + 16;
492
+ char *r = sp_str_alloc(want);
493
+ memcpy(r, s, la);
494
+ memcpy(r + la, t, lb);
495
+ r[la + lb] = 0;
496
+ sp_str_set_len(r, la + lb);
497
+ /* Preserve BINARY when growing the receiver. A compatible text append to
498
+ an ASCII-only binary receiver is the exception: text_append has already
499
+ selected UTF-8, so do not mark the replacement buffer binary. */
500
+ if (sp_str_is_binary(s) && !text_append) sp_str_mark_binary(r);
501
+ return r;
502
+ }
503
+ /* The same append for the first `lb` bytes of `t`, for the append form of an
504
+ interpolation (emit_interp_append): the length was taken when the part was
505
+ evaluated, so a part that is `s` itself contributes what it held then even
506
+ if an earlier part's append grew `s` in place. */
507
+ const char *sp_str_append_grow_n(const char *s, const char *t, size_t lb) {SP_GC_ROOT_STR(s);SP_GC_ROOT_STR(t);
508
+ if (!s) return t ? sp_str_from_bytes(t, lb) : sp_str_empty;
509
+ if (!t || lb == 0) return s;
510
+ size_t la = sp_str_byte_len(s);
511
+ unsigned char m = ((const unsigned char *)s)[-1];
512
+ if (m == 0xfe || m == 0xfc) {
513
+ sp_str_hdr *h = ((sp_str_hdr *)(s - 1)) - 1;
514
+ size_t total = (size_t)(h->size & SP_STR_SIZE_MASK);
515
+ size_t cap = total > sizeof(sp_str_hdr) + 2 ? total - sizeof(sp_str_hdr) - 2 : 0;
516
+ if (la + lb <= cap) {
517
+ memmove((char *)s + la, t, lb); /* t may point into s (a self-append) */
518
+ ((char *)s)[la + lb] = 0;
519
+ sp_str_lcache_drop(s);
520
+ sp_str_set_len((char *)s, la + lb);
521
+ return s;
522
+ }
523
+ }
524
+ size_t want = (la + lb) * 2 + 16;
525
+ char *r = sp_str_alloc(want);
526
+ memcpy(r, s, la);
527
+ memcpy(r + la, t, lb);
528
+ r[la + lb] = 0;
529
+ sp_str_set_len(r, la + lb);
530
+ if (sp_str_is_binary(s)) sp_str_mark_binary(r);
531
+ return r;
532
+ }
533
+ const char*sp_str_substr(const char*s,sp_int start,sp_int len){SP_GC_ROOT_STR(s);if(!s)sp_nil_recv("[]");if(len<=0){char*r=sp_str_alloc_raw(1);r[0]=0;sp_str_set_len(r,0);return sp_str_bin_from(r,s);}if(start<0)start=0;char*r=sp_str_alloc_raw(len+1);memcpy(r,s+start,len);r[len]=0;sp_str_set_len(r,(size_t)len);return sp_str_bin_from(r,s);}
534
+ const char*sp_str_delete_suffix(const char*s,const char*p){SP_GC_ROOT_STR(s);SP_GC_ROOT_STR(p);if(!s)sp_nil_recv("delete_suffix");if(!p)return s;size_t sl=sp_str_byte_len(s),pl=sp_str_byte_len(p);if(pl<=sl&&memcmp(s+sl-pl,p,pl)==0){char*r=sp_str_alloc_raw(sl-pl+1);memcpy(r,s,sl-pl);r[sl-pl]=0;sp_str_set_len(r,sl-pl);return r;}char*r=sp_str_alloc_raw(sl+1);memcpy(r,s,sl+1);sp_str_set_len(r,sl);return r;}
535
+ /* strip / lstrip / rstrip. CRuby strips the set "\0\t\n\v\f\r " from the
536
+ ends -- i.e. isspace() plus the NUL byte. Use sp_str_byte_len (not
537
+ strlen) so a heap string carrying an embedded NUL (e.g. from pack /
538
+ concat) is measured and stripped correctly; the result is a
539
+ length-tracked heap string so any interior NUL survives. (A frozen
540
+ literal with an embedded NUL is still truncated at the C level -- that
541
+ needs length-tracked literals, out of scope.) */
542
+ const char*sp_str_strip(const char*s){SP_GC_ROOT_STR(s);if(!s)sp_nil_recv("strip");size_t len=sp_str_byte_len(s);size_t a=0;while(a<len&&(isspace((unsigned char)s[a])||s[a]=='\0'))a++;size_t b=len;while(b>a&&(isspace((unsigned char)s[b-1])||s[b-1]=='\0'))b--;size_t n=b-a;char*r=sp_str_alloc(n);memcpy(r,s+a,n);r[n]=0;return r;}
543
+ const char*sp_str_chomp(const char*s){SP_GC_ROOT_STR(s);if(!s)sp_nil_recv("chomp");size_t l=sp_str_byte_len(s);if(l>=2&&s[l-2]=='\r'&&s[l-1]=='\n')l-=2;else if(l>0&&s[l-1]=='\n')l--;else if(l>0&&s[l-1]=='\r')l--;char*r=sp_str_alloc_raw(l+1);memcpy(r,s,l);r[l]=0;sp_str_set_len(r,l);return r;}
544
+ /* Issue #881: `"hello!".chomp("!")` strips the explicit separator.
545
+ Empty sep strips any trailing newlines (CRuby paragraph mode).
546
+ NULL sep is caller's responsibility (codegen routes nil to a
547
+ no-op before calling). */
548
+ const char *sp_str_chomp_sep(const char *s, const char *sep) {SP_GC_ROOT_STR(s);SP_GC_ROOT_STR(sep);
549
+ if (!s) sp_nil_recv("chomp");
550
+ size_t l = sp_str_byte_len(s); /* byte-exact (#4527) */
551
+ /* "\n" is the record separator's own case: it also takes a trailing "\r\n" or "\r" */
552
+ if (sep && sep[0] == '\n' && sp_str_byte_len(sep) == 1) return sp_str_chomp(s);
553
+ if (!sep || !*sep) {
554
+ /* Empty sep = paragraph mode: strip trailing \r\n pairs and
555
+ standalone \n's, but NOT standalone \r's. A trailing \r that
556
+ is not part of a \r\n pair stops the stripping. */
557
+ while (l > 0) {
558
+ if (l >= 2 && s[l-2] == '\r' && s[l-1] == '\n') { l -= 2; continue; }
559
+ if (s[l-1] == '\n') { l--; continue; }
560
+ break;
561
+ }
562
+ }
563
+ else {
564
+ size_t sl = sp_str_byte_len(sep);
565
+ if (sl <= l && memcmp(s + l - sl, sep, sl) == 0) l -= sl;
566
+ }
567
+ char *r = sp_str_alloc_raw(l + 1);
568
+ memcpy(r, s, l);
569
+ r[l] = 0;
570
+ sp_str_set_len(r, l);
571
+ return r;
572
+ }
573
+ const char*sp_str_chop(const char*s){SP_GC_ROOT_STR(s);if(!s)sp_nil_recv("chop");size_t l=sp_str_byte_len(s);if(l>0){if(l>=2&&s[l-2]=='\r'&&s[l-1]=='\n')l-=2;else{l--;/* back up over any UTF-8 continuation bytes to the char boundary (#3085) */while(l>0&&((unsigned char)s[l]&0xC0)==0x80)l--;}}char*r=sp_str_alloc_raw(l+1);memcpy(r,s,l);r[l]=0;sp_str_set_len(r,l);return r;}
574
+ /* String#chr: the first character (a whole UTF-8 char, not a byte), "" for "" (#3083). */
575
+ const char*sp_str_chr(const char*s){SP_GC_ROOT_STR(s);if(!s)sp_nil_recv("chr");if(*s==0)return sp_str_empty;int n=sp_utf8_advance(s);char*r=sp_str_alloc_raw((size_t)n+1);memcpy(r,s,(size_t)n);r[n]=0;sp_str_set_len(r,(size_t)n);return r;}
576
+ /* The character index at byte offset `byteoff`; preserves -1 (no match) and 0.
577
+ Used to report a regexp match position in characters, not bytes (#3056). */
578
+ sp_int sp_str_byte_to_char(const char*s,sp_int byteoff){if(byteoff<=0||!s)return byteoff;sp_int bl=(sp_int)sp_str_byte_len(s);sp_int ci=0;for(sp_int i=0;i<byteoff&&i<bl;ci++)i+=sp_utf8_advance(s+i);/* to the byte length, not the first NUL (#4527) */return ci;}
579
+ /* Issue #797: NULL guards on receiver + needle for the chunk of
580
+ string functions that read directly into a non-checked strlen. */
581
+ sp_bool sp_str_include(const char*s,const char*sub){if(!sub)sp_raise_cls("TypeError","no implicit conversion of nil into String");if(!s)sp_nil_recv("include?");return sp_bytestr(s,sp_str_byte_len(s),sub,sp_str_byte_len(sub))!=NULL;}
582
+ sp_bool sp_str_start_with(const char*s,const char*p){if(!p)sp_raise_cls("TypeError","no implicit conversion of nil into String");if(!s)sp_nil_recv("start_with?");return strncmp(s,p,strlen(p))==0;}
583
+ sp_bool sp_str_end_with(const char*s,const char*suf){if(!suf)sp_raise_cls("TypeError","no implicit conversion of nil into String");if(!s)sp_nil_recv("end_with?");size_t ls=sp_str_byte_len(s),lsuf=sp_str_byte_len(suf);/* byte-exact: a NUL is a byte, not the end (#4527) */if(lsuf>ls)return FALSE;return memcmp(s+ls-lsuf,suf,lsuf)==0;}
584
+ /* partition: [before, sep, after] at the first sep; no match -> [s, "", ""]. */
585
+ /* partition: [before, sep, after] at the first sep; no match -> [s, "", ""]. */
586
+ sp_StrArray *sp_str_partition(const char *s, const char *sep) {
587
+ SP_GC_ROOT_STR(s); SP_GC_ROOT_STR(sep);
588
+ sp_StrArray *r = sp_StrArray_new();
589
+ SP_GC_ROOT(r); /* keep r (and its pushed slices) live across the byteslice allocs */
590
+ /* byte-exact: strlen/strstr cannot express a NUL in the separator or the
591
+ subject, so partitioning on one found nothing and answered ["", "", s] */
592
+ sp_int bl = (sp_int)sp_str_byte_len(s), sl = (sp_int)sp_str_byte_len(sep);
593
+ const char *f = sl > 0 ? sp_bytestr(s, (size_t)bl, sep, (size_t)sl) : s;
594
+ if (!f) { sp_StrArray_push(r, sp_str_dup(s)); sp_StrArray_push(r, sp_str_dup(sp_str_empty)); sp_StrArray_push(r, sp_str_dup(sp_str_empty)); return r; }
595
+ sp_int pre = (sp_int)(f - s);
596
+ sp_StrArray_push(r, sp_str_byteslice(s, 0, pre));
597
+ sp_StrArray_push(r, sp_str_byteslice(s, pre, sl));
598
+ sp_StrArray_push(r, sp_str_byteslice(s, pre + sl, bl - pre - sl));
599
+ return r;
600
+ }
601
+ /* rpartition: split at the last sep; no match -> ["", "", s]. */
602
+ /* rpartition: split at the last sep; no match -> ["", "", s]. */
603
+ sp_StrArray *sp_str_rpartition(const char *s, const char *sep) {
604
+ SP_GC_ROOT_STR(s); SP_GC_ROOT_STR(sep);
605
+ sp_StrArray *r = sp_StrArray_new();
606
+ SP_GC_ROOT(r); /* keep r (and its pushed slices) live across the byteslice allocs */
607
+ sp_int bl = (sp_int)sp_str_byte_len(s), sl = (sp_int)sp_str_byte_len(sep);
608
+ const char *last = sl == 0 ? s + bl : NULL; /* an empty sep matches at the end: [s, "", ""] */
609
+ if (sl > 0) { const char *p = s, *e = s + bl;
610
+ while (p < e) { const char *f2 = sp_bytestr(p, (size_t)(e - p), sep, (size_t)sl); if (!f2) break; last = f2; p = f2 + 1; } }
611
+ if (!last) { sp_StrArray_push(r, sp_str_dup(sp_str_empty)); sp_StrArray_push(r, sp_str_dup(sp_str_empty)); sp_StrArray_push(r, sp_str_dup(s)); return r; }
612
+ sp_int pre = (sp_int)(last - s);
613
+ sp_StrArray_push(r, sp_str_byteslice(s, 0, pre));
614
+ sp_StrArray_push(r, sp_str_byteslice(s, pre, sl));
615
+ sp_StrArray_push(r, sp_str_byteslice(s, pre + sl, bl - pre - sl));
616
+ return r;
617
+ }
618
+ /* String#lines: split on \n but PRESERVE the trailing newline on each
619
+ line (CRuby semantics). The last line keeps its terminator if present;
620
+ if absent, it just stops there. Empty string returns an empty array.
621
+ `end` is computed once at entry so a string with no newlines avoids
622
+ a redundant strlen call on the trailing piece. */
623
+ /* String#lines(sep) / #each_line(sep): segments each keeping the separator
624
+ (the trailing fragment keeps whatever remains). An empty sep is CRuby's
625
+ "paragraph mode" -- not supported here; callers gate on a non-empty sep. */
626
+ /* lines(sep, chomp: true): the separator's run is trimmed off each piece
627
+ (#3546) */
628
+ sp_StrArray*sp_str_lines_sep_chomp(const char*s,const char*sep){SP_GC_ROOT_STR(s);SP_GC_ROOT_STR(sep);
629
+ sp_StrArray*src=sp_str_lines_sep(s,sep);SP_GC_ROOT(src);
630
+ sp_StrArray*a=sp_StrArray_new();SP_GC_ROOT(a);
631
+ size_t sl=sep?strlen(sep):0;
632
+ for(sp_int i=0;i<sp_StrArray_length(src);i++){
633
+ const char*e=sp_StrArray_get(src,i);size_t n=e?strlen(e):0;
634
+ if(sl==0){while(n>0&&e[n-1]=='\n')n--;}
635
+ else if(n>=sl&&memcmp(e+n-sl,sep,sl)==0)n-=sl;
636
+ char*r=sp_str_alloc_raw(n+1);memcpy(r,e,n);r[n]=0;sp_str_set_len(r,n);
637
+ sp_StrArray_push(a,r);
638
+ }
639
+ return a;
640
+ }
641
+ sp_StrArray*sp_str_lines_sep(const char*s,const char*sep){SP_GC_ROOT_STR(s);SP_GC_ROOT_STR(sep);sp_StrArray*a=sp_StrArray_new();SP_GC_ROOT(a);if(!s||*s==0)return a;size_t sl=sep?strlen(sep):0;/* an empty separator is CRuby's paragraph mode: split after each run of two
642
+ or more newlines, keeping the run with the paragraph (#3546) */
643
+ if(sl==0){const char*end0=s+strlen(s);const char*p0=s;while(p0<end0){const char*q=p0;while(q<end0){if(*q=='\n'){const char*b1=q;while(b1<end0&&*b1=='\n')b1++;if(b1-q>=2)break;q=b1;continue;}q++;}size_t n0=(q>=end0)?(size_t)(end0-p0):(size_t)(q-p0)+2;char*r0=sp_str_alloc_raw(n0+1);memcpy(r0,p0,n0);r0[n0]=0;sp_str_set_len(r0,n0);sp_StrArray_push(a,r0);if(q>=end0)break;{const char*b2=q;while(b2<end0&&*b2=='\n')b2++;p0=b2;}}return a;}const char*end=s+strlen(s);const char*p=s;while(p<end){const char*hit=strstr(p,sep);size_t n=hit?(size_t)(hit-p)+sl:(size_t)(end-p);char*r=sp_str_alloc_raw(n+1);memcpy(r,p,n);r[n]=0;sp_str_set_len(r,n);sp_StrArray_push(a,r);if(!hit)break;p=hit+sl;}return a;}
644
+ sp_StrArray*sp_str_lines(const char*s){SP_GC_ROOT_STR(s);sp_StrArray*a=sp_StrArray_new();size_t bl=sp_str_byte_len(s);if(bl==0)return a;SP_GC_ROOT(a);const char*end=s+bl;const char*p=s;while(p<end){const char*nl=memchr(p,'\n',(size_t)(end-p));size_t n=nl?(size_t)(nl-p+1):(size_t)(end-p);char*r=sp_str_alloc_raw(n+1);memcpy(r,p,n);r[n]=0;sp_str_set_len(r,n);sp_StrArray_push(a,r);if(!nl)break;p=nl+1;}return a;}
645
+ sp_StrArray*sp_str_lines_chomp(const char*s){SP_GC_ROOT_STR(s);sp_StrArray*a=sp_StrArray_new();size_t bl=sp_str_byte_len(s);if(bl==0)return a;SP_GC_ROOT(a);const char*end=s+bl;const char*p=s;while(p<end){const char*nl=memchr(p,'\n',(size_t)(end-p));size_t n=nl?(size_t)(nl-p):(size_t)(end-p);if(nl&&nl>s&&nl[-1]=='\r')n--;char*r=sp_str_alloc_raw(n+1);memcpy(r,p,n);r[n]=0;sp_str_set_len(r,n);sp_StrArray_push(a,r);if(!nl)break;p=nl+1;}return a;}
646
+ /* String#byteslice(start,len): byte-indexed (unlike the char-indexed
647
+ sp_str_sub_range). Negative start counts back from the byte length.
648
+ Out-of-range yields the empty string rather than CRuby nil. */
649
+ const char*sp_str_byteslice(const char*s,sp_int start,sp_int len){SP_GC_ROOT_STR(s);if(!s)sp_nil_recv("byteslice");sp_int bl=(sp_int)sp_str_byte_len(s);if(start<0)start+=bl;if(start<0||start>bl||len<0){return NULL;/* out of range -> nil (#2333) */}if(len>bl-start)len=bl-start;if(len<=0){return sp_str_is_binary(s)?sp_str_empty_binary():&("\xff" "")[1];}char*r=sp_str_alloc_raw(len+1);memcpy(r,s+start,len);r[len]=0;sp_str_set_len(r,(size_t)len);return sp_str_bin_from(r,s);}
650
+ /* Single-argument String#byteslice(i): the 1-byte substring at byte index i,
651
+ or nil when i is outside [0, bytesize) (the boundary i == bytesize has no
652
+ byte, unlike the two-argument form's empty slice) (#2333). */
653
+ const char*sp_str_byteslice1(const char*s,sp_int i){SP_GC_ROOT_STR(s);if(!s)sp_nil_recv("byteslice");sp_int bl=(sp_int)sp_str_byte_len(s);if(i<0)i+=bl;if(i<0||i>=bl)return NULL;return sp_str_byteslice(s,i,1);}
654
+ /* String#byteslice(range): resolve beginless/endless/negative endpoints
655
+ against the bytesize, then slice; a begin past the bytesize is nil (#2348). */
656
+ const char*sp_str_byteslice_range(const char*s,sp_int lo,sp_int hi,int excl,int lo_none,int hi_none){SP_GC_ROOT_STR(s);if(!s)sp_nil_recv("byteslice");sp_int bl=(sp_int)sp_str_byte_len(s);if(lo_none)lo=0;else if(lo<0)lo+=bl;if(lo<0||lo>bl)return NULL;sp_int end;if(hi_none)end=bl;else{end=hi;if(end<0)end+=bl;if(!excl)end+=1;}if(end>bl)end=bl;sp_int len=end-lo;if(len<0)len=0;return sp_str_byteslice(s,lo,len);}
657
+ /* String#bytesplice(start,len,str): replace the byte span with str and
658
+ return the new value (the codegen arm rebinds the receiver, so the
659
+ result stands in for CRuby's mutated self). CRuby's IndexError contracts:
660
+ negative length raises, start is checked against the byte length with
661
+ the original (pre-normalization) index in the message; an overlong span
662
+ clamps to the end. */
663
+ const char*sp_str_bytesplice(const char*s,sp_int start,sp_int len,const char*val){SP_GC_ROOT_STR(s);SP_GC_ROOT_STR(val);if(!s)sp_nil_recv("bytesplice");if(!val)val=&("\xff" "")[1];sp_int bl=(sp_int)sp_str_byte_len(s);if(len<0)sp_raise_cls("IndexError",sp_sprintf("negative length %lld",(long long)len));sp_int st=start<0?start+bl:start;if(st<0||st>bl)sp_raise_cls("IndexError",sp_sprintf("index %lld out of string",(long long)start));if(len>bl-st)len=bl-st;size_t vl=sp_str_byte_len(val);char*r=sp_str_alloc_raw((size_t)(bl-len)+vl+1);memcpy(r,s,(size_t)st);memcpy(r+st,val,vl);memcpy(r+st+vl,s+st+len,(size_t)(bl-st-len));r[(size_t)(bl-len)+vl]=0;sp_str_set_len(r,(size_t)(bl-len)+vl);return r;}
664
+ /* String#ascii_only?: 1 iff every byte is in the 7-bit ASCII range. */
665
+ int sp_str_ascii_only(const char*s){sp_int bl=(sp_int)sp_str_byte_len(s);for(sp_int i=0;i<bl;i++){if((unsigned char)s[i]>=0x80)return 0;}return 1;}
666
+ const char*sp_str_format_strarr(const char*fmt,sp_StrArray*a){SP_GC_ROOT_STR(fmt);size_t cap=strlen(fmt)+64;char*buf=(char*)malloc(cap);if(!buf){perror("malloc");exit(1);}size_t out=0;sp_int idx=0;const char*p=fmt;while(*p){if(*p=='%'){if(p[1]=='s'){const char*s=(idx<a->len)?a->data[idx]:"";size_t sl=sp_str_byte_len(s); /* the byte length: an embedded NUL is a byte of the string (#4632) */if(out+sl>=cap){size_t nc=((out+sl)*2)+1;char*nb=(char*)realloc(buf,nc);if(!nb){free(buf);perror("realloc");exit(1);}buf=nb;cap=nc;}memcpy(buf+out,s,sl);out+=sl;idx++;p+=2;}
667
+ else if(p[1]=='%'){if(out+1>=cap){size_t nc=cap*2;char*nb=(char*)realloc(buf,nc);if(!nb){free(buf);perror("realloc");exit(1);}buf=nb;cap=nc;}buf[out++]='%';p+=2;}
668
+ else{if(out+1>=cap){size_t nc=cap*2;char*nb=(char*)realloc(buf,nc);if(!nb){free(buf);perror("realloc");exit(1);}buf=nb;cap=nc;}buf[out++]=*p++;}}
669
+ else{if(out+1>=cap){size_t nc=cap*2;char*nb=(char*)realloc(buf,nc);if(!nb){free(buf);perror("realloc");exit(1);}buf=nb;cap=nc;}buf[out++]=*p++;}}buf[out]=0;char*r=sp_str_alloc(out);memcpy(r,buf,out);free(buf);return r;}
670
+ /* byte-exact search and lengths: strstr/strlen stop at an embedded NUL, so a
671
+ pattern or subject holding one matched the wrong place or not at all (the
672
+ opportunistic NUL policy -- fix what is met). */
673
+ /* slice!(str): s without the first occurrence of pat, or s itself when there
674
+ is none. Unlike sub it sets no `$~`, as CRuby's slice! sets none. */
675
+ const char*sp_str_remove_first(const char*s,const char*pat){SP_GC_ROOT_STR(s);SP_GC_ROOT_STR(pat);if(!s||!pat)return s;size_t pl=sp_str_byte_len(pat),sl=sp_str_byte_len(s);const char*f=sp_bytestr(s,sl,pat,pl);if(!f)return s;size_t n=(size_t)(f-s);char*r=sp_str_alloc_raw(sl-pl+1);memcpy(r,s,n);memcpy(r+n,f+pl,sl-n-pl);r[sl-pl]=0;sp_str_set_len(r,sl-pl);return r;}
676
+ const char*sp_str_sub(const char*s,const char*pat,const char*rep){SP_GC_ROOT_STR(s);SP_GC_ROOT_STR(pat);SP_GC_ROOT_STR(rep);if(!s)sp_nil_recv("sub");if(!pat||!rep)return s;size_t pl0=sp_str_byte_len(pat),sl0=sp_str_byte_len(s);const char*f=sp_bytestr(s,sl0,pat,pl0);if(!f){if(sp_re_track_last)sp_re_clear_last_match();return sp_str_dup(s);}sp_re_sub_matched=1;if(sp_re_track_last)sp_re_set_lit_match(s,(sp_int)(f-s),(sp_int)(f-s+pl0));char*rep_exp=sp_str_rep_expand(rep,pat,pl0);if(rep_exp)rep=rep_exp;size_t pl=pl0,rl=rep_exp?strlen(rep):sp_str_byte_len(rep),sl=sl0;char*r=sp_str_alloc_raw(sl-pl+rl+1);size_t n=f-s;memcpy(r,s,n);memcpy(r+n,rep,rl);memcpy(r+n+rl,f+pl,sl-n-pl);r[sl-pl+rl]=0;sp_str_set_len(r,sl-pl+rl);if(rep_exp)free(rep_exp);return r;}
677
+ const char*sp_str_capitalize(const char*s){SP_GC_ROOT_STR(s);if(!s)sp_nil_recv("capitalize");size_t l=sp_str_byte_len(s);char*r=sp_str_alloc_raw(l*3+1);size_t oi=0;int first=1;for(size_t i=0;i<l;){uint32_t cp;int n=sp_utf8_decode(s+i,&cp);i+=(size_t)n;if(first){uint32_t u=sp_uc_toupper(cp);if(cp==0xDF){r[oi++]='S';r[oi++]='S';}
678
+ else oi+=(size_t)sp_utf8_encode(u,r+oi);first=0;}
679
+ else oi+=(size_t)sp_utf8_encode(sp_uc_tolower(cp),r+oi);}r[oi]=0;sp_str_set_len(r,oi);return r;}
680
+ /* The longest string a caller-sized operation builds: String#*, and the
681
+ center/ljust/rjust family. Past it the request raises ArgumentError, which a
682
+ program can rescue, instead of reaching the allocator -- a failed
683
+ allocation ends the process (sp_oom_die) -- and sp_str_hdr's 32-bit length
684
+ fields never see a value they would truncate. `have` is what the result
685
+ already holds, `add` what the operation appends. */
686
+ static const size_t sp_str_max_bytes=(size_t)1<<30;
687
+ static void sp_str_check_size(size_t have, size_t add){if(have>sp_str_max_bytes||add>sp_str_max_bytes-have)sp_raise_cls("ArgumentError","string size too big");}
688
+ /* The justify family's size rule, in 64 bits on every target, as CRuby
689
+ answers it: a byte total past a long is ArgumentError ("argument too big");
690
+ one past what this runtime builds (sp_str_max_bytes) is the NoMemoryError
691
+ CRuby raises when the allocation fails -- raised here, so it can be rescued
692
+ and the allocator never sees the request. UINT64_MAX stands for a total
693
+ that overflowed. Answers the class to raise, NULL when the result fits. */
694
+ static const char *sp_justify_size_err(uint64_t have, uint64_t add) {
695
+ if (add == UINT64_MAX || add >= (uint64_t)INT64_MAX - have) return "ArgumentError";
696
+ if (have + add > (uint64_t)sp_str_max_bytes) return "NoMemoryError";
697
+ return NULL;
698
+ }
699
+ static void sp_justify_size_raise(const char *cls) {
700
+ sp_raise_cls(cls, cls[0] == 'A' ? "argument too big" : "failed to allocate memory");
701
+ }
702
+ /* The bytes `need` characters of a cycling pad take, counted per cycle
703
+ rather than per character; UINT64_MAX on overflow. */
704
+ static uint64_t sp_pad_bytes(const uint32_t *cps, size_t pn, uint64_t need) {
705
+ char tmp[4]; uint64_t cyc = 0, part = 0, rem = need % pn;
706
+ for (size_t i = 0; i < pn; i++) {
707
+ uint64_t b = (uint64_t)sp_utf8_encode(cps[i], tmp);
708
+ cyc += b; if (i < rem) part += b;
709
+ }
710
+ uint64_t full = need / pn;
711
+ if (cyc && full > (UINT64_MAX - part) / cyc) return UINT64_MAX;
712
+ return full * cyc + part;
713
+ }
714
+ const char*sp_str_repeat(const char*s,sp_int n){SP_GC_ROOT_STR(s);
715
+ if(n<0) sp_raise_cls("ArgumentError","negative argument");
716
+ if(!s)sp_nil_recv("*");if(n<=0)return sp_str_is_binary(s)?sp_str_empty_binary():sp_str_empty;
717
+ /* the real byte length, not strlen: `0.chr * 3` is three NUL bytes, and a
718
+ repeat of "a\0b" carries the NUL through each copy (#3473) */
719
+ size_t l=sp_str_byte_len(s);
720
+ if(l==0) return sp_str_is_binary(s)?sp_str_empty_binary():sp_str_empty;
721
+ if((size_t)n>SIZE_MAX/l) sp_raise_cls("ArgumentError","string size too big");
722
+ size_t total=(size_t)n*l;
723
+ sp_str_check_size(0,total);
724
+ char*r=sp_str_alloc_raw(total+1);
725
+ for(sp_int i=0;i<n;i++)memcpy(r+(l*i),s,l);
726
+ r[total]=0;
727
+ sp_str_set_len(r,total);
728
+ return sp_str_bin_from(r,s);
729
+ }
730
+ /* root `s` before the IntArray_new GC-alloc: the argument is often a fresh
731
+ unrooted temp (`data[off, n].bytes` in a fused times.map -- doom's
732
+ Colormap.load), and a collection triggered by the alloc freed it mid-call,
733
+ yielding an empty result exactly on GC-boundary iterations. */
734
+ sp_IntArray*sp_str_bytes(const char*s){SP_GC_ROOT_STR(s);sp_IntArray*a=sp_IntArray_new();if(!s)sp_nil_recv("bytes");size_t n=sp_str_byte_len(s);for(size_t i=0;i<n;i++)sp_IntArray_push(a,(sp_int)(unsigned char)s[i]);return a;}
735
+ const char*sp_str_lstrip(const char*s){SP_GC_ROOT_STR(s);if(!s)sp_nil_recv("lstrip");size_t len=sp_str_byte_len(s);size_t a=0;while(a<len&&(isspace((unsigned char)s[a])||s[a]=='\0'))a++;size_t n=len-a;char*r=sp_str_alloc(n);memcpy(r,s+a,n);r[n]=0;return r;}
736
+ const char*sp_str_rstrip(const char*s){SP_GC_ROOT_STR(s);if(!s)sp_nil_recv("rstrip");size_t len=sp_str_byte_len(s);size_t b=len;while(b>0&&(isspace((unsigned char)s[b-1])||s[b-1]=='\0'))b--;char*r=sp_str_alloc(b);memcpy(r,s,b);r[b]=0;return r;}
737
+ /* String#b: a fresh, unfrozen copy tagged BINARY (ASCII-8BIT). The dup alone
738
+ left the copy UTF-8, so `"caf\u00e9".b.inspect` printed the characters where
739
+ CRuby prints the escaped bytes, and length kept counting characters. pack
740
+ already marks its answer; #b is the sibling that did not. */
741
+ const char*sp_str_b(const char*s){SP_GC_ROOT_STR(s);if(!s)sp_nil_recv("b");char*r=(char*)sp_str_dup(s);sp_str_mark_binary(r);return r;}
742
+ const char*sp_str_dup(const char*s){SP_GC_ROOT_STR(s);if(!s)return NULL;size_t l=sp_str_byte_len(s);char*r=sp_str_alloc(l);memcpy(r,s,l);if(sp_str_is_binary(s))sp_str_mark_binary(r);return r;}
743
+
744
+ /* ===================== utf8-dependent transforms ===================== */
745
+ sp_int sp_str_count_chars(const char *s, size_t bl) {
746
+ const char *p = s, *end = s + bl;
747
+ sp_int n = 0;
748
+ while (p + 8 <= end) {
749
+ uint64_t w;
750
+ memcpy(&w, p, sizeof(w));
751
+ if (w & 0x8080808080808080ULL) break;
752
+ p += 8; n += 8;
753
+ }
754
+ while (p < end) {
755
+ if ((unsigned char)*p < 0x80) { p++; n++; }
756
+ else { p += sp_utf8_advance(p); n++; }
757
+ }
758
+ return n;
759
+ }
760
+ /* Length in "units" for String#length/size, validating and counting in a
761
+ single walk: count UTF-8 codepoints while the bytes stay valid UTF-8, but
762
+ fall back to the raw byte count the moment an invalid byte appears
763
+ (near-certain for genuine binary data, e.g. a binary-mode File.read of a WAD
764
+ lump) -- matching Ruby's ASCII-8BIT semantics for binary I/O without a
765
+ separate per-string encoding tag. Folding validation into the count keeps
766
+ String#length off a two-pass hot path; valid text still costs one walk. */
767
+ static sp_int sp_str_count_units(const char *s, size_t bl) {
768
+ const unsigned char *p = (const unsigned char *)s, *end = p + bl;
769
+ sp_int n = 0;
770
+ for (;;) {
771
+ /* ASCII fast path: 8 bytes at a time, each a valid single-unit codepoint */
772
+ while (p + 8 <= end) {
773
+ uint64_t w;
774
+ memcpy(&w, p, sizeof(w));
775
+ if (w & 0x8080808080808080ULL) break;
776
+ p += 8; n += 8;
777
+ }
778
+ if (p >= end) break;
779
+ unsigned c = *p;
780
+ if (c < 0x80) { p++; n++; continue; }
781
+ int extra; unsigned cp, min;
782
+ if ((c & 0xE0) == 0xC0) { extra = 1; cp = c & 0x1F; min = 0x80; }
783
+ else if ((c & 0xF0) == 0xE0) { extra = 2; cp = c & 0x0F; min = 0x800; }
784
+ else if ((c & 0xF8) == 0xF0) { extra = 3; cp = c & 0x07; min = 0x10000; }
785
+ else return (sp_int)bl; /* invalid lead byte -> count bytes */
786
+ p++;
787
+ if (p + extra > end) return (sp_int)bl;
788
+ for (int i = 0; i < extra; i++) {
789
+ if ((*p & 0xC0) != 0x80) return (sp_int)bl;
790
+ cp = (cp << 6) | (*p & 0x3F);
791
+ p++;
792
+ }
793
+ if (cp < min || (cp >= 0xD800 && cp <= 0xDFFF) || cp > 0x10FFFF) return (sp_int)bl;
794
+ n++;
795
+ }
796
+ return n;
797
+ }
798
+ sp_int sp_str_length(const char*s){
799
+ if (!s) return 0;
800
+ /* binary bytes are one unit each, whatever they happen to spell in UTF-8;
801
+ a string a scan has proved 7-bit counts the same way, and answering from
802
+ the header skips the pointer-keyed cache probe entirely */
803
+ { size_t fb; if (sp_str_fixed_width(s, &fb)) return (sp_int)fb; }
804
+ if (sp_str_is_binary(s)) return (sp_int)sp_str_byte_len(s);
805
+ if (!sp_str_cacheable(s)) return sp_str_count_units(s, sp_str_byte_len(s));
806
+ unsigned h = sp_str_lcache_hash(s);
807
+ for (unsigned w = 0; w < SP_STR_LCACHE_WAYS; w++)
808
+ if (sp_str_lcache[h + w].s == s) return sp_str_lcache[h + w].char_len;
809
+ size_t bl = sp_str_byte_len(s);
810
+ sp_int n = sp_str_count_units(s, bl);
811
+ /* the walk just told us: char count == byte count means every byte is
812
+ below 0x80, so remember it in the header and never walk (or probe) again */
813
+ if ((size_t)n == bl) sp_str_mark_ascii7(s);
814
+ /* Evict the cheapest entry to recompute, not the oldest: a scan over one long
815
+ subject allocates a stream of short strings, and measuring those pushed the
816
+ subject out of its bucket over and over -- each miss another walk of the
817
+ whole thing. A short newcomer never displaces a much longer incumbent. */
818
+ unsigned victim = h;
819
+ for (unsigned w = 1; w < SP_STR_LCACHE_WAYS; w++) {
820
+ if (!sp_str_lcache[h + w].s) { victim = h + w; break; }
821
+ if (sp_str_lcache[h + w].byte_len < sp_str_lcache[victim].byte_len) victim = h + w;
822
+ }
823
+ if (sp_str_lcache[victim].s && sp_str_lcache[victim].byte_len > bl * 8) return n;
824
+ sp_str_lcache[victim].s = s;
825
+ sp_str_lcache[victim].byte_len = bl;
826
+ sp_str_lcache[victim].char_len = n;
827
+ return n;
828
+ }
829
+ /* The byte length comes from sp_str_byte_len, which knows every marker: this
830
+ spelled out 0xfe/0xfc itself and fell to strlen for the rest, so a
831
+ header-bearing string whose first byte is NUL -- a 1-byte slice of a BINARY
832
+ string, served from the static table -- measured 0 and raised. */
833
+ sp_int sp_str_ord(const char*s){if(!s)sp_nil_recv("ord");
834
+ if(sp_str_byte_len(s)==0)sp_raise_cls("ArgumentError","empty string");
835
+ uint32_t cp;sp_utf8_decode(s,&cp);return(sp_int)cp;}
836
+ size_t sp_utf8_byte_offset(const char*s,sp_int char_idx){
837
+ if (!s || char_idx <= 0) return 0;
838
+ /* one unit per byte on a binary string, so indexing and slicing agree with
839
+ the byte-counted #length */
840
+ { size_t fb;
841
+ if (sp_str_fixed_width(s, &fb)) { size_t off = (size_t)char_idx; return off > fb ? fb : off; } }
842
+ if (sp_str_is_binary(s)) {
843
+ size_t bl = sp_str_byte_len(s), off = (size_t)char_idx;
844
+ return off > bl ? bl : off;
845
+ }
846
+ if (sp_str_cacheable(s)) {
847
+ unsigned h = sp_str_lcache_hash(s);
848
+ for (unsigned w = 0; w < SP_STR_LCACHE_WAYS; w++) {
849
+ if (sp_str_lcache[h + w].s != s) continue;
850
+ if ((size_t)sp_str_lcache[h + w].char_len != sp_str_lcache[h + w].byte_len) break;
851
+ size_t off = (size_t)char_idx;
852
+ return off > sp_str_lcache[h + w].byte_len ? sp_str_lcache[h + w].byte_len : off;
853
+ }
854
+ }
855
+ /* Walk char_idx code points or stop at byte_len, whichever comes first.
856
+ Bounding on byte_len (instead of "*p != 0") keeps the walk correct
857
+ past an embedded NUL byte -- a heap string with the 0xfe/0xfc marker
858
+ carries its real length in the sp_str_hdr, so a NUL byte inside the
859
+ payload no longer terminates the walk prematurely. For an external
860
+ 0xff literal sp_str_byte_len falls back to strlen which legitimately
861
+ stops at NUL, matching the prior behaviour. */
862
+ size_t blen = sp_str_byte_len(s);
863
+ const char *p = s;
864
+ const char *end = s + blen;
865
+ while (char_idx > 0 && p < end) {
866
+ p += sp_utf8_advance(p);
867
+ char_idx--;
868
+ }
869
+ if (p > end) p = end;
870
+ return (size_t)(p - s);
871
+ }
872
+ uint32_t*sp_utf8_decode_all(const char*s,size_t*out_n){size_t cap=8,n=0;uint32_t*cps=(uint32_t*)malloc(cap*sizeof(uint32_t));if(!cps){*out_n=0;return NULL;}const char*p=s;const char*end=s?s+sp_str_byte_len(s):s;/* byte-exact: a NUL is a code point (#4527) */while(s&&p<end){if(n>=cap){size_t nc=cap*2;uint32_t*nx=(uint32_t*)realloc(cps,nc*sizeof(uint32_t));if(!nx){free(cps);*out_n=0;return NULL;}cps=nx;cap=nc;}uint32_t cp;p+=sp_utf8_decode(p,&cp);cps[n++]=cp;}*out_n=n;return cps;}
873
+ /* Length-aware variant: honors `bl` bytes so a NUL in the charset (e.g.
874
+ `"a\0b".count(itself)`) is a real member rather than a terminator. */
875
+ uint32_t*sp_utf8_decode_charset_n(const char*s,size_t bl,size_t*out_n){SP_GC_ROOT_STR(s);
876
+ size_t cap=16,n=0;
877
+ uint32_t*cps=(uint32_t*)malloc(cap*sizeof(uint32_t));
878
+ if(!cps){*out_n=0;return NULL;}
879
+ const char*p=s; const char*end=s+bl;
880
+ uint32_t prev=0; int has_prev=0;
881
+ while(s&&p<end){
882
+ uint32_t cp;
883
+ int len=sp_utf8_decode(p,&cp);
884
+ p+=len;
885
+ /* A backslash escapes the next character, so `"a\\-b"` is the three
886
+ members a, - and b rather than the range a..b (#3552). */
887
+ if(cp=='\\'&&p<end){
888
+ int elen=sp_utf8_decode(p,&cp);
889
+ p+=elen;
890
+ if(n>=cap){cap*=2;cps=(uint32_t*)realloc(cps,cap*sizeof(uint32_t));}
891
+ cps[n++]=cp;
892
+ prev=cp; has_prev=1;
893
+ continue;
894
+ }
895
+ /* Detect range: prev '-' next (but leading or trailing '-'
896
+ is literal). When current char is '-' and there's a next
897
+ non-'-' char and we have a prev, expand. */
898
+ if(cp=='-' && has_prev && p<end){
899
+ uint32_t hi;
900
+ int hi_len=sp_utf8_decode(p,&hi);
901
+ p+=hi_len;
902
+ if(hi>=prev){
903
+ /* Drop the prev we already wrote, re-emit the whole range. */
904
+ n--; /* undo prev */
905
+ for(uint32_t c=prev;c<=hi;c++){
906
+ if(n>=cap){cap*=2;cps=(uint32_t*)realloc(cps,cap*sizeof(uint32_t));}
907
+ cps[n++]=c;
908
+ }
909
+ has_prev=0;
910
+ continue;
911
+ }
912
+ /* Descending range (hi < prev) is an error in count/tr/delete/squeeze. */
913
+ free(cps);
914
+ sp_raise_cls("ArgumentError", "invalid range in string transliteration");
915
+ }
916
+ if(n>=cap){cap*=2;cps=(uint32_t*)realloc(cps,cap*sizeof(uint32_t));}
917
+ cps[n++]=cp;
918
+ prev=cp; has_prev=1;
919
+ }
920
+ *out_n=n;
921
+ return cps;
922
+ }
923
+ uint32_t*sp_utf8_decode_charset(const char*s,size_t*out_n){SP_GC_ROOT_STR(s);return sp_utf8_decode_charset_n(s,s?strlen(s):0,out_n);}
924
+ /* Reuse an existing StrArray for split, avoiding GC alloc.
925
+ Clears a->len and refills. Substring strings are still malloc'd. */
926
+ void sp_str_split_into(sp_StrArray*a,const char*s,const char*sep){
927
+ SP_GC_ROOT(a);
928
+ SP_GC_ROOT_STR(s);
929
+ SP_GC_ROOT_STR(sep);
930
+ a->len=0;
931
+ /* byte lengths and a byte-exact search: strlen/strstr stop at an embedded
932
+ NUL, so a subject or separator holding one split short (the opportunistic
933
+ NUL policy -- fix what is met). */
934
+ size_t bl=sp_str_byte_len(s);
935
+ if(bl==0)return;
936
+ size_t sl=sp_str_byte_len(sep);
937
+ if(sl==0){
938
+ for(size_t i=0;i<bl;){
939
+ int cn=sp_utf8_advance(s+i); if(cn<1)cn=1; if((size_t)cn>bl-i)cn=(int)(bl-i);
940
+ sp_str_split_push(a,s+i,(size_t)cn);
941
+ i+=(size_t)cn;
942
+ }
943
+ return;
944
+ }
945
+ const char*p=s;const char*se=s+bl;
946
+ while(1){
947
+ const char*f=sp_bytestr(p,(size_t)(se-p),sep,sl);
948
+ if(!f){
949
+ sp_str_split_push(a,p,(size_t)(se-p));
950
+ break;
951
+ }
952
+ size_t n=(size_t)(f-p);
953
+ sp_str_split_push(a,p,n);
954
+ p=f+sl;
955
+ }
956
+ }
957
+ /* String#undump: reverse of String#dump. The argument must be wrapped in
958
+ double quotes; the escapes dump can emit (\n \t \r \f \v \a \b \e \s \0
959
+ \" \\ \# \xHH \uHHHH \u{...}) are decoded back to bytes. The decoded
960
+ string is never longer than the dumped form, so one buffer suffices. */
961
+ const char*sp_str_undump(const char*s){SP_GC_ROOT_STR(s);
962
+ if(!s)sp_nil_recv("undump");
963
+ size_t n=strlen(s);
964
+ if(n<2||s[0]!='"'||s[n-1]!='"'){sp_raise_cls("RuntimeError","invalid dumped string");return sp_str_empty;}
965
+ const char*p=s+1;const char*pe=s+n-1;
966
+ char*out=sp_str_alloc_raw(n+1);size_t oi=0;
967
+ while(p<pe){
968
+ if(*p!='\\'){out[oi++]=*p++;continue;}
969
+ p++;if(p>=pe)break;
970
+ char c=*p++;
971
+ if(c=='n')out[oi++]='\n';else if(c=='t')out[oi++]='\t';else if(c=='r')out[oi++]='\r';
972
+ else if(c=='f')out[oi++]='\f';else if(c=='v')out[oi++]='\v';else if(c=='a')out[oi++]='\a';
973
+ else if(c=='b')out[oi++]='\b';else if(c=='e')out[oi++]='\033';else if(c=='s')out[oi++]=' ';
974
+ else if(c=='0')out[oi++]='\0';else if(c=='\\')out[oi++]='\\';else if(c=='"')out[oi++]='"';
975
+ else if(c=='#')out[oi++]='#';
976
+ else if(c=='x'){int v=0,k=0;while(k<2&&p<pe&&isxdigit((unsigned char)*p)){v=(v*16)+_sp_hexval((unsigned char)*p);p++;k++;}out[oi++]=(char)v;}
977
+ else if(c=='u'){
978
+ if(p<pe&&*p=='{'){p++;while(p<pe&&*p!='}'){while(p<pe&&*p==' ')p++;uint32_t cp=0;int k=0;while(k<8&&p<pe&&isxdigit((unsigned char)*p)){cp=(cp*16)+(uint32_t)_sp_hexval((unsigned char)*p);p++;k++;}char enc[4];int el=sp_utf8_encode(cp,enc);for(int j=0;j<el;j++)out[oi++]=enc[j];while(p<pe&&*p==' ')p++;}if(p<pe&&*p=='}')p++;}
979
+ else{uint32_t cp=0;int k=0;while(k<4&&p<pe&&isxdigit((unsigned char)*p)){cp=(cp*16)+(uint32_t)_sp_hexval((unsigned char)*p);p++;k++;}char enc[4];int el=sp_utf8_encode(cp,enc);for(int j=0;j<el;j++)out[oi++]=enc[j];}
980
+ }
981
+ else out[oi++]=c;
982
+ }
983
+ out[oi]=0;sp_str_set_len(out,oi);return out;
984
+ }
985
+ const char*sp_str_succ_impl(const char*s){SP_GC_ROOT_STR(s);if(!s)sp_nil_recv("succ");size_t l=sp_str_byte_len(s);if(l==0){char*r=sp_str_alloc_raw(1);r[0]=0;sp_str_set_len(r,0);return r;}/* Find start of last codepoint */size_t lc=l-1;while(lc>0&&((unsigned char)s[lc]&0xC0)==0x80)lc--;if((unsigned char)s[lc]>=0x80){/* Multibyte tail: increment its codepoint */uint32_t cp;sp_utf8_decode(s+lc,&cp);cp++;char enc[4];int el=sp_utf8_encode(cp,enc);char*r=sp_str_alloc_raw(lc+el+1);memcpy(r,s,lc);memcpy(r+lc,enc,el);r[lc+el]=0;sp_str_set_len(r,lc+(size_t)el);return r;}/* ASCII tail: CRuby's alnum-aware carry. The rightmost alphanumeric
986
+ increments; a wrap (9->0, z->a, Z->A) carries into the adjacent character
987
+ when it is alphanumeric of any class, else into the nearest alphanumeric
988
+ to the left of the SAME class (digit vs alpha); with no carry target left,
989
+ the wrapped class's carry character (1/a/A) is inserted at the wrap
990
+ position. A string with no alphanumerics increments its last byte. */
991
+ char*r=sp_str_alloc_raw(l+2);memcpy(r,s,l);r[l]=0;
992
+ #define SP_SUCC_AL(ch) (((ch)>='0'&&(ch)<='9')||((ch)>='a'&&(ch)<='z')||((ch)>='A'&&(ch)<='Z'))
993
+ sp_int i=(sp_int)l-1;
994
+ while(i>=0&&!SP_SUCC_AL((unsigned char)r[i]))i--;
995
+ if(i<0){r[l-1]=(char)((unsigned char)r[l-1]+1);sp_str_set_len(r,l);return r;}
996
+ for(;;){
997
+ unsigned char c=(unsigned char)r[i];
998
+ if(c!='9'&&c!='z'&&c!='Z'){r[i]=(char)(c+1);sp_str_set_len(r,l);return r;}
999
+ int dig=(c=='9');
1000
+ char ins=(c=='9')?'1':(c=='z')?'a':'A';
1001
+ r[i]=(c=='9')?'0':(c=='z')?'a':'A';
1002
+ if(i>0&&SP_SUCC_AL((unsigned char)r[i-1])){i--;continue;}
1003
+ sp_int j=i-1;
1004
+ while(j>=0&&!SP_SUCC_AL((unsigned char)r[j]))j--;
1005
+ if(j>=0){unsigned char cj=(unsigned char)r[j];int jdig=(cj>='0'&&cj<='9');if(jdig==dig){i=j;continue;}}
1006
+ memmove(r+i+1,r+i,l-(size_t)i+1);r[i]=ins;sp_str_set_len(r,l+1);return r;
1007
+ }
1008
+ #undef SP_SUCC_AL
1009
+ }
1010
+ /* The ASCII same-length carry paths in succ_impl allocate l+2 bytes (room for
1011
+ a prepend) but return a string of length l, leaving the heap header's len
1012
+ field one too large. Callers that read sp_str_byte_len (e.g. concat) then
1013
+ copy a trailing NUL. This wrapper normalizes the header len to strlen; succ
1014
+ never produces an embedded NUL so this is always correct. */
1015
+ /* succ_impl records the real length on every path now, so the old strlen
1016
+ normalisation here would UNDO it for a source holding a NUL. */
1017
+ const char*sp_str_succ(const char*s){SP_GC_ROOT_STR(s);return sp_str_succ_impl(s);}
1018
+ /* succ applied n times (n < 1: the string itself): the next member of an
1019
+ endless String range walked by step(n) */
1020
+ const char*sp_str_succ_n(const char*s,sp_int n){SP_GC_ROOT_STR(s);for(sp_int i=0;i<n;i++)s=sp_str_succ_impl(s);return s;}
1021
+ sp_StrArray*sp_str_split(const char*s,const char*sep){if(!s)sp_nil_recv("split");
1022
+ SP_GC_ROOT_STR(s);
1023
+ SP_GC_ROOT_STR(sep);
1024
+ sp_StrArray*a=sp_StrArray_new();
1025
+ sp_str_split_into(a,s,sep);
1026
+ return a;
1027
+ }
1028
+ /* Same as sp_str_split but removes trailing empty strings
1029
+ (CRuby default limit behavior: split without limit drops
1030
+ trailing empties; split(sep, -1) keeps them). */
1031
+ sp_StrArray*sp_str_split_drop_trailing(const char*s,const char*sep){SP_GC_ROOT_STR(s);SP_GC_ROOT_STR(sep);if(!sep||(sep[0]==' '&&sp_str_byte_len(sep)==1))return sp_str_split_ws(s);sp_StrArray*a=sp_str_split(s,sep);/* an EMPTY tail, not one that merely starts with NUL: `"\0x\0".split("x")`
1032
+ dropped both halves when the test was s[0]==0. */
1033
+ while(a->len>0&&sp_str_byte_len(a->data[a->len-1])==0)a->len--;return a;}
1034
+ /* `s.split(sep, n)` with explicit limit. Positive n caps the result
1035
+ at n elements: the last element holds the unsplit remainder.
1036
+ n == 0 means "no limit" and drops trailing empty strings (same as
1037
+ the no-arg default); n < 0 means "no limit" but keeps trailing
1038
+ empties. Empty separator splits into Unicode characters, then keeps
1039
+ one empty field after them if the limit leaves room for it.
1040
+ Issue #619 puzzle 2. */
1041
+ sp_StrArray*sp_str_split_limit(const char*s,const char*sep,sp_int n){if(!s)sp_nil_recv("split");
1042
+ if(!sep||(sep[0]==' '&&sp_str_byte_len(sep)==1))return sp_str_split_ws_limit(s,n);
1043
+ if(n==0)return sp_str_split_drop_trailing(s,sep);
1044
+ if(n<0){sp_StrArray*a=sp_str_split(s,sep);SP_GC_ROOT(a);
1045
+ /* a non-empty String split at an empty separator ends in one empty field, as with `n` positive below */
1046
+ if(sp_str_byte_len(s)&&!sp_str_byte_len(sep))sp_str_split_push(a,"",0);return a;}
1047
+ SP_GC_ROOT_STR(s);
1048
+ SP_GC_ROOT_STR(sep);
1049
+ sp_StrArray*a=sp_StrArray_new();
1050
+ SP_GC_ROOT(a);
1051
+ if(*s==0)return a;
1052
+ size_t sl=strlen(sep);
1053
+ if(sl==0){
1054
+ const char*p=s;
1055
+ sp_int k=0;
1056
+ while(*p&&k<n-1){
1057
+ int cn=sp_utf8_advance(p);
1058
+ sp_str_split_push(a,p,(size_t)cn);
1059
+ p+=cn;
1060
+ k++;
1061
+ }
1062
+ if(*p)sp_str_split_push(a,p,strlen(p));
1063
+ else if(!sp_str_byte_len(sep)&&p==s+sp_str_byte_len(s))sp_str_split_push(a,"",0); /* every character is out: the limit leaves room for the empty field after them */
1064
+ return a;
1065
+ }
1066
+ const char*p=s;
1067
+ sp_int k=0;
1068
+ while(k<n-1){
1069
+ const char*f=strstr(p,sep);
1070
+ if(!f)break;
1071
+ size_t m=f-p;
1072
+ sp_str_split_push(a,p,m);
1073
+ p=f+sl;
1074
+ k++;
1075
+ }
1076
+ sp_str_split_push(a,p,strlen(p));
1077
+ return a;
1078
+ }
1079
+ #define SP_SPLIT_WS(c) ((c)==' '||(c)=='\t'||(c)=='\n'||(c)=='\r'||(c)=='\f'||(c)=='\v')
1080
+ /* `s.split` / `s.split(nil)` -- whitespace mode: split on runs of
1081
+ ASCII whitespace, skip leading whitespace. Issue #507: the no-arg
1082
+ form previously emitted `sp_str_split(s, 0)` and segfaulted at
1083
+ strlen(NULL). */
1084
+ sp_StrArray*sp_str_split_ws(const char*s){if(!s)sp_nil_recv("split");
1085
+ SP_GC_ROOT_STR(s);
1086
+ sp_StrArray*a=sp_StrArray_new();
1087
+ SP_GC_ROOT(a);
1088
+ /* by byte length, not strlen: a NUL is an ordinary character here */
1089
+ const char*p=s,*e=s+sp_str_byte_len(s);
1090
+ while(p<e&&SP_SPLIT_WS(*p))p++;
1091
+ while(p<e){
1092
+ const char*start=p;
1093
+ while(p<e&&!SP_SPLIT_WS(*p))p++;
1094
+ size_t n=p-start;
1095
+ sp_str_split_push(a,start,n);
1096
+ while(p<e&&SP_SPLIT_WS(*p))p++;
1097
+ }
1098
+ return a;
1099
+ }
1100
+ /* Whitespace-mode split with an explicit limit. A single-space separator is
1101
+ special in Ruby: it skips leading whitespace and collapses separator runs,
1102
+ including when the separator comes from a variable or a limit is present.
1103
+ Positive limits preserve the unsplit remainder; negative limits retain a
1104
+ trailing empty field. */
1105
+ sp_StrArray*sp_str_split_ws_limit(const char*s,sp_int n){if(!s)sp_nil_recv("split");
1106
+ if(n==0)return sp_str_split_ws(s);
1107
+ SP_GC_ROOT_STR(s);
1108
+ sp_StrArray*a=sp_StrArray_new();
1109
+ SP_GC_ROOT(a);
1110
+ const char*e=s+sp_str_byte_len(s);
1111
+ if(e==s)return a;
1112
+ if(n==1){sp_str_split_push(a,s,(size_t)(e-s));return a;}
1113
+ const char*p=s;
1114
+ while(p<e&&SP_SPLIT_WS(*p))p++;
1115
+ if(p==e){sp_str_split_push(a,p,0);return a;}
1116
+ sp_int k=0;
1117
+ while(p<e){
1118
+ const char*start=p;
1119
+ while(p<e&&!SP_SPLIT_WS(*p))p++;
1120
+ sp_str_split_push(a,start,(size_t)(p-start));
1121
+ k++;
1122
+ const char*sep_start=p;
1123
+ while(p<e&&SP_SPLIT_WS(*p))p++;
1124
+ if(p==e){if(p>sep_start&&(n<0||(n>0&&k<n)))sp_str_split_push(a,p,0);break;}
1125
+ if(n>0&&k==n-1){sp_str_split_push(a,p,(size_t)(e-p));break;}
1126
+ }
1127
+ return a;
1128
+ }
1129
+ #undef SP_SPLIT_WS
1130
+ /* String-pattern String#scan. Regexp scans use sp_re_scan; this path handles
1131
+ a String argument, returning non-overlapping literal matches. The empty
1132
+ pattern matches at every UTF-8 character boundary, including both ends. */
1133
+ sp_StrArray*sp_str_scan(const char*s,const char*pat){if(!s)sp_nil_recv("scan");
1134
+ SP_GC_ROOT_STR(s);SP_GC_ROOT_STR(pat);
1135
+ sp_StrArray*a=sp_StrArray_new();SP_GC_ROOT(a);
1136
+ if(!pat)sp_raise_cls("TypeError","wrong argument type nil (expected Regexp)");
1137
+ size_t pl=strlen(pat);
1138
+ /* `$~` is the last match, or nil when there was none */
1139
+ if(pl==0){
1140
+ const char*p=s;
1141
+ for(;;){sp_str_split_push(a,p,0);if(!*p)break;p+=sp_utf8_advance(p);}
1142
+ if(sp_re_track_last)sp_re_set_lit_match(s,(sp_int)(p-s),(sp_int)(p-s));
1143
+ return a;
1144
+ }
1145
+ const char*p=s,*f,*last=NULL;
1146
+ while((f=strstr(p,pat))!=NULL){sp_str_split_push(a,f,pl);last=f;p=f+pl;}
1147
+ if(sp_re_track_last){if(last)sp_re_set_lit_match(s,(sp_int)(last-s),(sp_int)(last-s+pl));else sp_re_clear_last_match();}
1148
+ return a;
1149
+ }
1150
+ /* One turn of `s.scan(pat) { }` whose block reads `$~`: sp_str_scan builds
1151
+ the rows up front, so each turn finds its own match again from byte `pos`
1152
+ and sets the registers to it (#3601). Answers where the next turn looks. */
1153
+ sp_int sp_str_scan_at(const char*s,const char*pat,sp_int pos){SP_GC_ROOT_STR(s);
1154
+ size_t pl=strlen(pat);
1155
+ const char*f=pl?strstr(s+pos,pat):s+pos;
1156
+ if(!f)return pos;
1157
+ sp_int at=(sp_int)(f-s);
1158
+ sp_re_set_lit_match(s,at,at+(sp_int)pl);
1159
+ if(pl)return at+(sp_int)pl;
1160
+ return *f?at+sp_utf8_advance(f):at+1;
1161
+ }
1162
+ /* String#gsub(pat, rep) for literal (non-regex) patterns. Issue #827: the
1163
+ result must come from sp_str_alloc, not a raw malloc buffer, because the
1164
+ GC's sp_mark_string writes the marker byte at offset -1 and would corrupt
1165
+ malloc metadata otherwise. Issue #850: an empty pattern inserts the
1166
+ replacement between every character (and at both ends). */
1167
+ /* Expand backslash sequences in a gsub/sub replacement against a fixed matched
1168
+ text `match` (mlen bytes). For a String pattern there are no capture groups,
1169
+ so \1-\9 expand to nothing; \\ -> \, \& and \0 -> the whole match; any other
1170
+ \c keeps both characters. Returns a malloc'd C string the caller frees.
1171
+ Returns NULL when the replacement has no backslash (caller uses rep as-is). */
1172
+ static char *sp_str_rep_expand(const char *rep, const char *match, size_t mlen) {
1173
+ if (!rep || !strchr(rep, '\\')) return NULL;
1174
+ size_t rl = strlen(rep);
1175
+ size_t cap = rl + mlen + 1, ol = 0;
1176
+ char *out = (char *)malloc(cap);
1177
+ for (size_t i = 0; i < rl; i++) {
1178
+ if (rep[i] == '\\' && i + 1 < rl) {
1179
+ char d = rep[i + 1]; i++;
1180
+ if (d == '\\') { if (ol + 1 >= cap) { cap = cap * 2 + 1; out = (char *)realloc(out, cap); } out[ol++] = '\\'; }
1181
+ else if (d == '&' || d == '0') { if (ol + mlen >= cap) { cap = (ol + mlen) * 2 + 1; out = (char *)realloc(out, cap); } memcpy(out + ol, match, mlen); ol += mlen; }
1182
+ else if (d >= '1' && d <= '9') { /* no capture groups: empty */ }
1183
+ else { if (ol + 2 >= cap) { cap = cap * 2 + 1; out = (char *)realloc(out, cap); } out[ol++] = '\\'; out[ol++] = d; }
1184
+ }
1185
+ else {
1186
+ if (ol + 1 >= cap) { cap = cap * 2 + 1; out = (char *)realloc(out, cap); }
1187
+ out[ol++] = rep[i];
1188
+ }
1189
+ }
1190
+ out[ol] = 0;
1191
+ return out;
1192
+ }
1193
+ const char*sp_str_gsub(const char*s,const char*pat,const char*rep){SP_GC_ROOT_STR(s);SP_GC_ROOT_STR(pat);SP_GC_ROOT_STR(rep);
1194
+ if(!s)sp_nil_recv("gsub");
1195
+ if(!pat||!rep)return s;
1196
+ /* byte lengths and a byte-exact search throughout: strlen/strstr stop at an
1197
+ embedded NUL, so a subject or pattern holding one was cut short (the
1198
+ opportunistic NUL policy). */
1199
+ size_t pl0=sp_str_byte_len(pat);
1200
+ char*rep_exp=sp_str_rep_expand(rep,pat,pl0);
1201
+ if(rep_exp)rep=rep_exp;
1202
+ size_t pl=pl0,rl=rep_exp?strlen(rep):sp_str_byte_len(rep),sl=sp_str_byte_len(s);
1203
+ if(pl==0){
1204
+ sp_re_sub_matched=1;
1205
+ /* Empty pattern: insert rep between every codepoint + at start/end.
1206
+ Result size: (chars+1) * rl + sl. */
1207
+ size_t cap=sl+(rl*(sl+1))+1;
1208
+ char*out=(char*)malloc(cap);
1209
+ size_t ol=0;
1210
+ memcpy(out+ol,rep,rl); ol+=rl;
1211
+ for(size_t i=0;i<sl;){
1212
+ int n=sp_utf8_advance(s+i); if(n<1)n=1; if((size_t)n>sl-i)n=(int)(sl-i);
1213
+ memcpy(out+ol,s+i,(size_t)n); ol+=(size_t)n;
1214
+ memcpy(out+ol,rep,rl); ol+=rl;
1215
+ i+=(size_t)n;
1216
+ }
1217
+ out[ol]=0;
1218
+ if(sp_re_track_last)sp_re_set_lit_match(s,(sp_int)sl,(sp_int)sl); /* `$~`: the last, empty, match at the end */
1219
+ char*r=sp_str_alloc(ol); memcpy(r,out,ol); sp_str_set_len(r,ol); free(out); if(rep_exp)free(rep_exp); return r;
1220
+ }
1221
+ size_t cap=(sl*2)+1;
1222
+ char*out=(char*)malloc(cap);
1223
+ size_t ol=0;
1224
+ const char*p=s;const char*se=s+sl;const char*last=NULL;
1225
+ while(p<se){
1226
+ const char*f=sp_bytestr(p,(size_t)(se-p),pat,pl);
1227
+ if(!f){size_t n=(size_t)(se-p);if(ol+n>=cap){cap=((ol+n)*2)+1;out=(char*)realloc(out,cap);}memcpy(out+ol,p,n);ol+=n;break;}
1228
+ sp_re_sub_matched=1;
1229
+ last=f;
1230
+ size_t n=(size_t)(f-p);
1231
+ if(ol+n+rl>=cap){cap=((ol+n+rl)*2)+1;out=(char*)realloc(out,cap);}
1232
+ memcpy(out+ol,p,n);ol+=n;
1233
+ memcpy(out+ol,rep,rl);ol+=rl;
1234
+ p=f+pl;
1235
+ }
1236
+ /* `$~` is the last match, or nil when there was none */
1237
+ if(sp_re_track_last){if(last)sp_re_set_lit_match(s,(sp_int)(last-s),(sp_int)(last-s+pl));else sp_re_clear_last_match();}
1238
+ out[ol]=0;char*r=sp_str_alloc(ol);memcpy(r,out,ol);sp_str_set_len(r,ol);free(out);if(rep_exp)free(rep_exp);return r;
1239
+ }
1240
+ /* `s.index(sub)` -- leftmost occurrence; returns a codepoint offset (not a
1241
+ byte offset), or -1 if not found. */
1242
+ /* The characters from `from` up to `to` inside s, and the byte offset of
1243
+ character n: one unit per byte for a BINARY string, whatever its bytes
1244
+ spell in UTF-8, so `"\xC3\xA9\xFF".b.index("\xFF".b)` is 2 (#5535). */
1245
+ static sp_int sp_str_units_between(const char *s, const char *from, const char *to) {
1246
+ if (sp_str_is_binary(s)) return (sp_int)(to - from);
1247
+ sp_int n = 0;
1248
+ while (from < to) { from += sp_utf8_advance(from); n++; }
1249
+ return n;
1250
+ }
1251
+ static size_t sp_str_unit_offset(const char *s, sp_int n) {
1252
+ return sp_str_is_binary(s) ? (size_t)n : sp_utf8_byte_offset(s, n);
1253
+ }
1254
+ sp_int sp_str_index(const char*s,const char*sub){if(!s)sp_nil_recv("index");if(!sub)sp_raise_cls("TypeError","no implicit conversion of nil into String");const char*f=sp_bytestr(s,sp_str_byte_len(s),sub,sp_str_byte_len(sub));if(!f)return -1;return sp_str_units_between(s,s,f);}
1255
+ /* Issue #758: NULL guard + bound the start so a negative result from
1256
+ sp_str_index doesn't underflow the source pointer. */
1257
+ sp_int sp_str_index_from(const char*s,const char*sub,sp_int start){if(!s)sp_nil_recv("index");sp_int cl=sp_str_length(s);if(start<0)start+=cl;if(start<0)return -1;if(start>cl)return -1;size_t boff=sp_str_unit_offset(s,start);const char*f=sp_bytestr(s+boff,sp_str_byte_len(s)-boff,sub,sp_str_byte_len(sub));if(!f)return -1;return start+sp_str_units_between(s,s+boff,f);}
1258
+ /* `s.rindex(sub)` -- rightmost occurrence of sub; returns a codepoint
1259
+ offset, or -1 if not found. Empty sub matches at the end. */
1260
+ sp_int sp_str_rindex(const char*s,const char*sub){if(!s)sp_nil_recv("rindex");if(!sub)sp_raise_cls("TypeError","no implicit conversion of nil into String");size_t sl=sp_str_byte_len(sub);if(sl==0)return sp_str_length(s);size_t hn=sp_str_byte_len(s);const char*end=s+hn;const char*last=NULL;const char*p=s;while(p<end){const char*f=sp_bytestr(p,(size_t)(end-p),sub,sl);if(!f)break;last=f;p=f+1;}if(!last)return -1;return sp_str_units_between(s,s,last);}
1261
+ /* `s.rindex(sub, pos)` -- rightmost occurrence at or before codepoint pos.
1262
+ Negative pos counts back from the char length; nil (SP_INT_NIL) on miss. */
1263
+ sp_int sp_str_rindex_from(const char*s,const char*sub,sp_int pos){if(!s)sp_nil_recv("rindex");if(!sub)sp_raise_cls("TypeError","no implicit conversion of nil into String");sp_int cl=sp_str_length(s);if(pos<0)pos=cl+pos;if(pos<0)return SP_INT_NIL;size_t sl=sp_str_byte_len(sub);if(sl==0){if(pos>=cl)return cl;return pos;}size_t hn=sp_str_byte_len(s);const char*end=s+hn;const char*p=s;sp_int best=-1;const char*r=s;sp_int cur_n=0;while(p<end){const char*f=sp_bytestr(p,(size_t)(end-p),sub,sl);if(!f)break;cur_n+=sp_str_units_between(s,r,f);r=f;if(cur_n>pos)break;best=cur_n;p=f+1;}return best<0?SP_INT_NIL:best;}
1264
+ /* byteindex/byterindex: like index/rindex but the result and the start/pos
1265
+ arguments are BYTE offsets, not codepoint indices. Byte-oriented throughout
1266
+ (sp_bytestr), so no utf8 walk is needed. nil (SP_INT_NIL) on miss. */
1267
+ sp_int sp_str_byteindex(const char*s,const char*sub){if(!s)sp_nil_recv("byteindex");if(!sub)sp_raise_cls("TypeError","no implicit conversion of nil into String");const char*f=sp_bytestr(s,sp_str_byte_len(s),sub,sp_str_byte_len(sub));return f?(sp_int)(f-s):SP_INT_NIL;}
1268
+ sp_int sp_str_byteindex_from(const char*s,const char*sub,sp_int start){if(!s)sp_nil_recv("byteindex");if(!sub)sp_raise_cls("TypeError","no implicit conversion of nil into String");sp_int bl=(sp_int)sp_str_byte_len(s);if(start<0)start+=bl;if(start<0||start>bl)return SP_INT_NIL;const char*f=sp_bytestr(s+start,(size_t)(bl-start),sub,sp_str_byte_len(sub));return f?(sp_int)(f-s):SP_INT_NIL;}
1269
+ sp_int sp_str_byterindex(const char*s,const char*sub){if(!s)sp_nil_recv("byterindex");if(!sub)sp_raise_cls("TypeError","no implicit conversion of nil into String");size_t sl=sp_str_byte_len(sub);size_t hn=sp_str_byte_len(s);if(sl==0)return(sp_int)hn;const char*end=s+hn;const char*last=NULL;const char*p=s;while(p<end){const char*f=sp_bytestr(p,(size_t)(end-p),sub,sl);if(!f)break;last=f;p=f+1;}return last?(sp_int)(last-s):SP_INT_NIL;}
1270
+ sp_int sp_str_byterindex_from(const char*s,const char*sub,sp_int pos){if(!s)sp_nil_recv("byterindex");if(!sub)sp_raise_cls("TypeError","no implicit conversion of nil into String");sp_int bl=(sp_int)sp_str_byte_len(s);if(pos<0)pos+=bl;if(pos<0)return SP_INT_NIL;if(pos>bl)pos=bl;size_t sl=sp_str_byte_len(sub);if(sl==0)return pos;const char*end=s+bl;const char*p=s;sp_int best=-1;while(p<end){const char*f=sp_bytestr(p,(size_t)(end-p),sub,sl);if(!f)break;sp_int off=(sp_int)(f-s);if(off>pos)break;best=off;p=f+1;}return best<0?SP_INT_NIL:best;}
1271
+ /* start/len are codepoint indices/counts. */
1272
+ /* `s[start, len]` char-indexed slice (UTF-8 aware). Negative start counts
1273
+ back from the char length. CRuby's nil contract: start past the length
1274
+ (or before -length) and a negative len return NULL; start == length and
1275
+ len == 0 return "". A single-char ASCII result aliases the per-process
1276
+ sp_char_cache so common indexing avoids an allocation. */
1277
+ const char*sp_str_sub_range(const char*s,sp_int start,sp_int len){SP_GC_ROOT_STR(s);if(!s)sp_nil_recv("[]");size_t _fb;int fixed=sp_str_fixed_width(s,&_fb);sp_int cl=fixed?(sp_int)_fb:sp_str_length(s);if(start<0)start+=cl;if(start<0||start>cl||len<0)return NULL;if(start==cl||len==0){return sp_str_is_binary(s)?sp_str_empty_binary():&("\xff" "")[1];}if(len>cl-start)len=cl-start;int bin=fixed?sp_str_is_binary(s):0;size_t boff=fixed?(size_t)start:sp_utf8_byte_offset(s,start);size_t blen_total=fixed?_fb:sp_str_byte_len(s);size_t bp=boff;sp_int rem=len;while(rem>0&&bp<blen_total){bp+=fixed?1:sp_utf8_advance(s+bp);rem--;}if(bp>blen_total)bp=blen_total;size_t bend=bp;size_t blen=bend-boff;if(bin&&len==1&&blen==1)return sp_bin_char((unsigned char)s[boff]);if(!bin&&len==1&&blen==1){unsigned char c=(unsigned char)s[boff];if(c!=0){if(!sp_char_cache_init){for(int i=0;i<256;i++){sp_char_cache[i][0]=(char)0xff;sp_char_cache[i][1]=(char)i;sp_char_cache[i][2]=0;}sp_char_cache_init=1;}return &sp_char_cache[c][1];}}char*r=sp_str_alloc_raw(blen+1);memcpy(r,s+boff,blen);r[blen]=0;sp_str_set_len(r,blen);if(bin)sp_str_mark_binary(r);return r;}
1278
+ /* Single-character `s[i]`. Returns NULL on out-of-bounds so the caller can
1279
+ yield CRuby's `"hello"[20] -> nil`. */
1280
+ const char*sp_str_char_at_or_nil(const char*s,sp_int i){SP_GC_ROOT_STR(s);if(!s)sp_nil_recv("[]");sp_int cl=sp_str_length(s);if(i<0)i+=cl;if(i<0||i>=cl)return NULL;return sp_str_sub_range(s,i,1);}
1281
+ const char*sp_str_sub_range_len(const char*s,sp_int cl,sp_int start,sp_int len){SP_GC_ROOT_STR(s);if(start<0)start+=cl;if(start<0||start>cl||len<0)return NULL;if(start==cl||len==0){return sp_str_is_binary(s)?sp_str_empty_binary():&("\xff" "")[1];}if(len>cl-start)len=cl-start;size_t _fb;int fixed=sp_str_fixed_width(s,&_fb);int bin=fixed?sp_str_is_binary(s):0;size_t boff=fixed?(size_t)start:sp_utf8_byte_offset(s,start);size_t blen_total=fixed?_fb:sp_str_byte_len(s);size_t bp=boff;sp_int rem=len;while(rem>0&&bp<blen_total){bp+=fixed?1:sp_utf8_advance(s+bp);rem--;}if(bp>blen_total)bp=blen_total;size_t bend=bp;size_t blen=bend-boff;if(bin&&len==1&&blen==1)return sp_bin_char((unsigned char)s[boff]);if(!bin&&len==1&&blen==1){unsigned char c=(unsigned char)s[boff];if(c!=0){if(!sp_char_cache_init){for(int i=0;i<256;i++){sp_char_cache[i][0]=(char)0xff;sp_char_cache[i][1]=(char)i;sp_char_cache[i][2]=0;}sp_char_cache_init=1;}return &sp_char_cache[c][1];}}char*r=sp_str_alloc_raw(blen+1);memcpy(r,s+boff,blen);r[blen]=0;sp_str_set_len(r,blen);if(bin)sp_str_mark_binary(r);return r;}
1282
+ const char*sp_str_sub_range_r(const char*s,sp_int start,sp_int end_,sp_int excl){SP_GC_ROOT_STR(s);if(!s)sp_nil_recv("[]");sp_int cl=sp_str_length(s);if(end_<0)end_+=cl;if(start<0)start+=cl;if(start<0)return NULL;if(end_>=cl){end_=cl;excl=1;}sp_int n=end_-start+(excl?0:1);if(n<0)n=0;return sp_str_sub_range_len(s,cl,start,n);}
1283
+ const char*sp_str_sub_range_len_r(const char*s,sp_int cl,sp_int start,sp_int end_,sp_int excl){SP_GC_ROOT_STR(s);if(end_<0)end_+=cl;if(start<0)start+=cl;if(start<0)return NULL;if(end_>=cl){end_=cl;excl=1;}sp_int n=end_-start+(excl?0:1);if(n<0)n=0;return sp_str_sub_range_len(s,cl,start,n);}
1284
+ /* walks by INDEX to the header length: `while(*p)` stopped at an embedded NUL
1285
+ and reversed only the prefix */
1286
+ const char*sp_str_reverse(const char*s){SP_GC_ROOT_STR(s);if(!s)sp_nil_recv("reverse");size_t bl=sp_str_byte_len(s);char*r=sp_str_alloc_raw(bl+1);size_t end=bl;for(size_t i=0;i<bl;){int cn=sp_utf8_advance(s+i);if(cn<1)cn=1;if((size_t)cn>bl-i)cn=(int)(bl-i);end-=(size_t)cn;memcpy(r+end,s+i,(size_t)cn);i+=(size_t)cn;}r[bl]=0;sp_str_set_len(r,bl);return r;}
1287
+ sp_int sp_str_count(const char*s,const char*chars){SP_GC_ROOT_STR(s);SP_GC_ROOT_STR(chars);if(!chars)sp_raise_cls("TypeError","no implicit conversion of nil into String");int negate=0;const char*csp=chars;if(*csp=='^'&&*(csp+1)){negate=1;csp++;}size_t setn;uint32_t*set=sp_utf8_decode_charset_n(csp,sp_str_byte_len(chars)-(size_t)(csp-chars),&setn);sp_int c=0;const char*p=s;const char*end=s+sp_str_byte_len(s);while(p<end){uint32_t cp;p+=sp_utf8_decode(p,&cp);int in_set=sp_utf8_set_has(set,setn,cp);if(negate)in_set=!in_set;if(in_set)c++;}free(set);return c;}
1288
+ sp_int sp_str_count_n(const char*s,const char**chars,sp_int n){SP_GC_ROOT_STR(s);if(n<=0)return 0;size_t*setns=(size_t*)malloc(n*sizeof(size_t));uint32_t**sets=(uint32_t**)malloc(n*sizeof(uint32_t*));int*negs=(int*)malloc(n*sizeof(int));for(sp_int i=0;i<n;i++){if(!chars[i])sp_raise_cls("TypeError","no implicit conversion of nil into String");const char*cs=chars[i];negs[i]=0;if(*cs=='^'&&*(cs+1)){negs[i]=1;cs++;}sets[i]=sp_utf8_decode_charset_n(cs,sp_str_byte_len(chars[i])-(size_t)(cs-chars[i]),&setns[i]);}sp_int c=0;const char*p=s;const char*end=s+sp_str_byte_len(s);while(p<end){uint32_t cp;p+=sp_utf8_decode(p,&cp);int all=1;for(sp_int i=0;i<n;i++){int in_set=sp_utf8_set_has(sets[i],setns[i],cp);if(negs[i])in_set=!in_set;if(!in_set){all=0;break;}}if(all)c++;}for(sp_int i=0;i<n;i++)free(sets[i]);free(sets);free(setns);free(negs);return c;}
1289
+ /* The codepoints walked to the recorded byte length, as sp_str_chars walks:
1290
+ a BINARY String's are its bytes, and a NUL is a character like any other.
1291
+ String#codepoints with a block yields these. */
1292
+ sp_IntArray*sp_str_codepoints_all(const char*s){SP_GC_ROOT_STR(s);sp_IntArray*a=sp_IntArray_new();if(!s)sp_nil_recv("codepoints");SP_GC_ROOT(a);int bin=sp_str_is_binary(s);const char*end=s+sp_str_byte_len(s);const char*p=s;while(p<end){uint32_t cp;int n=1;if(bin)cp=(unsigned char)*p;else n=sp_utf8_decode(p,&cp);if(p+n>end)n=(int)(end-p);sp_IntArray_push(a,(sp_int)cp);p+=n;}return a;}
1293
+ sp_IntArray*sp_str_codepoints(const char*s){SP_GC_ROOT_STR(s);sp_IntArray*a=sp_IntArray_new();if(!s)sp_nil_recv("codepoints");const char*p=s;while(*p){uint32_t cp;int n=sp_utf8_decode(p,&cp);sp_IntArray_push(a,(sp_int)cp);p+=n;}return a;}
1294
+ /* walk to the recorded byte length rather than to the first NUL: a NUL is an
1295
+ ordinary one-byte character and the characters after it are real (#3473) */
1296
+ sp_StrArray*sp_str_chars(const char*s){SP_GC_ROOT_STR(s);sp_StrArray*a=sp_StrArray_new();if(!s)sp_nil_recv("chars");SP_GC_ROOT(a);int bin=sp_str_is_binary(s);const char*end=s+sp_str_byte_len(s);const char*p=s;while(p<end){int n=bin?1:sp_utf8_advance(p);if(p+n>end)n=(int)(end-p);char*c=sp_str_alloc(n);memcpy(c,p,n);c[n]=0;sp_StrArray_push(a,c);p+=n;}return a;}
1297
+ /* Issue #798: guard NULL inputs (CRuby treats nil/no-op gracefully). */
1298
+ const char*sp_str_tr(const char*s,const char*from,const char*to){SP_GC_ROOT_STR(s);SP_GC_ROOT_STR(from);SP_GC_ROOT_STR(to);if(!s)sp_nil_recv("tr");if(!from||!to)return s;int negate=0;const char*fp=from;if(*fp=='^'&&*(fp+1)){negate=1;fp++;}size_t fn,tn;uint32_t*fcps=sp_utf8_decode_charset_n(fp,sp_str_byte_len(from)-(size_t)(fp-from),&fn);uint32_t*tcps=sp_utf8_decode_charset_n(to,sp_str_byte_len(to),&tn);size_t bl=sp_str_byte_len(s);size_t cap=((bl*4))+1;char*buf=(char*)malloc(cap);size_t n=0;const char*p=s,*pe=s+bl;while(p<pe){uint32_t cp;int cn=sp_utf8_decode(p,&cp);size_t mi=fn;for(size_t j=0;j<fn;j++)if(fcps[j]==cp){mi=j;break;}int in_set=(mi<fn);if(negate)in_set=!in_set;if(in_set&&tn>0){uint32_t rep=negate?tcps[tn-1]:(mi<tn?tcps[mi]:tcps[tn-1]);n+=sp_utf8_encode(rep,buf+n);}
1299
+ else if(in_set){}
1300
+ else{memcpy(buf+n,p,cn);n+=cn;}p+=cn;}buf[n]=0;char*r=sp_str_alloc(n);memcpy(r,buf,n+1);free(buf);free(fcps);free(tcps);return r;}
1301
+ const char*sp_str_tr_s(const char*s,const char*from,const char*to){SP_GC_ROOT_STR(s);SP_GC_ROOT_STR(from);SP_GC_ROOT_STR(to);
1302
+ if(!s)sp_nil_recv("tr_s");
1303
+ if(!from||!to)return s;
1304
+ int negate=0;const char*fp=from;
1305
+ if(*fp=='^'&&*(fp+1)){negate=1;fp++;}
1306
+ size_t fn,tn;
1307
+ uint32_t*fcps=sp_utf8_decode_charset_n(fp,sp_str_byte_len(from)-(size_t)(fp-from),&fn);
1308
+ uint32_t*tcps=sp_utf8_decode_charset_n(to,sp_str_byte_len(to),&tn);
1309
+ size_t bl=strlen(s);
1310
+ size_t cap=(((bl*4)))+1;
1311
+ char*buf=(char*)malloc(cap);
1312
+ size_t n=0;
1313
+ const char*p=s;
1314
+ uint32_t last_emit=0; int has_last=0; int last_was_translated=0;
1315
+ while(*p){
1316
+ uint32_t cp; int cn=sp_utf8_decode(p,&cp);
1317
+ size_t mi=fn;
1318
+ for(size_t j=0;j<fn;j++)if(fcps[j]==cp){mi=j;break;}
1319
+ int in_set=(mi<fn);
1320
+ if(negate)in_set=!in_set;
1321
+ uint32_t emit_cp;
1322
+ int translated=0;
1323
+ if(in_set){
1324
+ if(tn>0){
1325
+ emit_cp=negate?tcps[tn-1]:(mi<tn?tcps[mi]:tcps[tn-1]);
1326
+ translated=1;
1327
+ }
1328
+ else {
1329
+ p+=cn; continue;
1330
+ }
1331
+ }
1332
+ else {
1333
+ emit_cp=cp;
1334
+ translated=0;
1335
+ }
1336
+ /* Squeeze only when both the previous and current emit were
1337
+ translated, AND the emitted codepoints match. */
1338
+ if(has_last && last_was_translated && translated && last_emit==emit_cp){
1339
+ /* skip */
1340
+ }
1341
+ else {
1342
+ n+=sp_utf8_encode(emit_cp,buf+n);
1343
+ last_emit=emit_cp;
1344
+ has_last=1;
1345
+ last_was_translated=translated;
1346
+ }
1347
+ p+=cn;
1348
+ }
1349
+ buf[n]=0;
1350
+ char*r=sp_str_alloc(n);
1351
+ memcpy(r,buf,n+1);
1352
+ free(buf); free(fcps); free(tcps);
1353
+ return r;
1354
+ }
1355
+ const char*sp_str_delete(const char*s,const char*chars){SP_GC_ROOT_STR(s);SP_GC_ROOT_STR(chars);if(!s)sp_nil_recv("delete");if(!chars)return s;int negate=0;const char*csp=chars;if(*csp=='^'&&*(csp+1)){negate=1;csp++;}size_t setn;uint32_t*set=sp_utf8_decode_charset_n(csp,sp_str_byte_len(chars)-(size_t)(csp-chars),&setn);size_t bl=sp_str_byte_len(s);char*buf=(char*)malloc(bl+1);if(!buf)sp_oom_die();size_t n=0;const char*p=s,*pe=s+bl;while(p<pe){uint32_t cp;int cn=sp_utf8_decode(p,&cp);int in_set=sp_utf8_set_has(set,setn,cp);if(negate)in_set=!in_set;if(!in_set){memcpy(buf+n,p,cn);n+=cn;}p+=cn;}buf[n]=0;free(set);char*r=sp_str_alloc(n);memcpy(r,buf,n+1);free(buf);return r;}
1356
+ const char*sp_str_squeeze(const char*s){SP_GC_ROOT_STR(s);if(!s)sp_nil_recv("squeeze");size_t bl=sp_str_byte_len(s);char*r=sp_str_alloc_raw(bl+1);size_t n=0;uint32_t prev=0xFFFFFFFFu;const char*p=s,*pe=s+bl;while(p<pe){uint32_t cp;int cn=sp_utf8_decode(p,&cp);if(cp!=prev){memcpy(r+n,p,cn);n+=cn;prev=cp;}p+=cn;}r[n]=0;sp_str_set_len(r,n);return r;}
1357
+ const char*sp_str_squeeze_chars(const char*s,const char*cs){SP_GC_ROOT_STR(s);SP_GC_ROOT_STR(cs);if(!s)sp_nil_recv("squeeze");if(!cs)return sp_str_squeeze(s);int negate=0;const char*csp=cs;if(*csp=='^'&&*(csp+1)){negate=1;csp++;}size_t fn;uint32_t*fcps=sp_utf8_decode_charset_n(csp,sp_str_byte_len(cs)-(size_t)(csp-cs),&fn);size_t bl=sp_str_byte_len(s);char*r=sp_str_alloc_raw(bl+1);size_t n=0;uint32_t prev=0xFFFFFFFFu;const char*p=s,*pe=s+bl;while(p<pe){uint32_t cp;int cn=sp_utf8_decode(p,&cp);int in_set=0;for(size_t j=0;j<fn;j++)if(fcps[j]==cp){in_set=1;break;}if(negate)in_set=!in_set;if(!in_set){memcpy(r+n,p,cn);n+=cn;prev=0xFFFFFFFFu;}
1358
+ else if(cp!=prev){memcpy(r+n,p,cn);n+=cn;prev=cp;}p+=cn;}r[n]=0;sp_str_set_len(r,n);free(fcps);return r;}
1359
+ const char*sp_str_delete_n(const char*s,const char**chars,sp_int n){SP_GC_ROOT_STR(s);if(!s)return sp_str_empty;if(n<=0)return s;size_t*setns=(size_t*)malloc(n*sizeof(size_t));uint32_t**sets=(uint32_t**)malloc(n*sizeof(uint32_t*));int*negs=(int*)malloc(n*sizeof(int));for(sp_int i=0;i<n;i++){if(!chars[i])sp_raise_cls("TypeError","no implicit conversion of nil into String");const char*cs=chars[i];negs[i]=0;if(*cs=='^'&&*(cs+1)){negs[i]=1;cs++;}sets[i]=sp_utf8_decode_charset_n(cs,sp_str_byte_len(chars[i])-(size_t)(cs-chars[i]),&setns[i]);}size_t bl=sp_str_byte_len(s);char*buf=(char*)malloc(bl+1);if(!buf)sp_oom_die();size_t m=0;const char*p=s,*pe=s+bl;while(p<pe){uint32_t cp;int cn=sp_utf8_decode(p,&cp);int all=1;for(sp_int i=0;i<n;i++){int in_set=sp_utf8_set_has(sets[i],setns[i],cp);if(negs[i])in_set=!in_set;if(!in_set){all=0;break;}}if(!all){memcpy(buf+m,p,cn);m+=cn;}p+=cn;}buf[m]=0;for(sp_int i=0;i<n;i++)free(sets[i]);free(sets);free(setns);free(negs);char*r=sp_str_alloc(m);memcpy(r,buf,m+1);free(buf);return r;}
1360
+ const char*sp_str_squeeze_n(const char*s,const char**chars,sp_int n){SP_GC_ROOT_STR(s);if(!s)return sp_str_empty;if(n<=0)return sp_str_squeeze(s);size_t*setns=(size_t*)malloc(n*sizeof(size_t));uint32_t**sets=(uint32_t**)malloc(n*sizeof(uint32_t*));int*negs=(int*)malloc(n*sizeof(int));for(sp_int i=0;i<n;i++){if(!chars[i])sp_raise_cls("TypeError","no implicit conversion of nil into String");const char*cs=chars[i];negs[i]=0;if(*cs=='^'&&*(cs+1)){negs[i]=1;cs++;}sets[i]=sp_utf8_decode_charset_n(cs,sp_str_byte_len(chars[i])-(size_t)(cs-chars[i]),&setns[i]);}size_t bl=sp_str_byte_len(s);char*r=sp_str_alloc_raw(bl+1);size_t m=0;uint32_t prev=0xFFFFFFFFu;const char*p=s,*pe=s+bl;while(p<pe){uint32_t cp;int cn=sp_utf8_decode(p,&cp);int all=1;for(sp_int i=0;i<n;i++){int in_set=sp_utf8_set_has(sets[i],setns[i],cp);if(negs[i])in_set=!in_set;if(!in_set){all=0;break;}}if(!all){memcpy(r+m,p,cn);m+=cn;prev=0xFFFFFFFFu;}
1361
+ else if(cp!=prev){memcpy(r+m,p,cn);m+=cn;prev=cp;}p+=cn;}r[m]=0;sp_str_set_len(r,m);for(sp_int i=0;i<n;i++)free(sets[i]);free(sets);free(setns);free(negs);return r;}
1362
+ /* String#scrub!: replace invalid bytes IN PLACE. CRuby raises FrozenError on
1363
+ a frozen receiver only when it would actually change it -- a frozen string
1364
+ with nothing to scrub comes back unchanged (#3338). */
1365
+ const char *sp_str_scrub_bang(const char *s, const char *repl) {
1366
+ SP_GC_ROOT_STR(s); SP_GC_ROOT_STR(repl);
1367
+ if (!s) return s;
1368
+ const char *r = sp_str_scrub(s, repl);
1369
+ if (r == s || sp_str_eq(r, s)) return s;
1370
+ sp_str_check_mutable(s);
1371
+ return r;
1372
+ }
1373
+ /* The length of the UTF-8 character at p (n bytes left), or of the invalid
1374
+ sequence there, *bad set: a lead byte and the continuation bytes that fit
1375
+ it before one that does not, as CRuby takes them (Unicode's maximal
1376
+ subpart), so a truncated "\xE3\x81" is one sequence and an overlong
1377
+ "\xC0\xAF" or a surrogate "\xED\xA0\x80" is one per byte. */
1378
+ static size_t sp_utf8_scan1(const unsigned char *p, size_t n, int *bad) {
1379
+ unsigned char c = p[0], lo = 0x80, hi = 0xBF;
1380
+ int need;
1381
+ *bad = 0;
1382
+ if (c < 0x80) return 1;
1383
+ if (c >= 0xC2 && c <= 0xDF) need = 1;
1384
+ else if (c >= 0xE0 && c <= 0xEF) { need = 2; if (c == 0xE0) lo = 0xA0; else if (c == 0xED) hi = 0x9F; }
1385
+ else if (c >= 0xF0 && c <= 0xF4) { need = 3; if (c == 0xF0) lo = 0x90; else if (c == 0xF4) hi = 0x8F; }
1386
+ else { *bad = 1; return 1; }
1387
+ size_t k = 1;
1388
+ for (; k <= (size_t)need; k++) {
1389
+ if (k >= n || p[k] < lo || p[k] > hi) { *bad = 1; return k; }
1390
+ lo = 0x80; hi = 0xBF;
1391
+ }
1392
+ return k;
1393
+ }
1394
+ /* String#scrub's walk: the offset of the first invalid sequence of s (bl
1395
+ bytes) at or after `from`, its length in *len; bl when there is none. */
1396
+ sp_int sp_str_scrub_bad(const char *s, sp_int bl, sp_int from, sp_int *len) {
1397
+ sp_int i = from;
1398
+ while (i < bl) {
1399
+ int bad;
1400
+ size_t k = sp_utf8_scan1((const unsigned char *)s + i, (size_t)(bl - i), &bad);
1401
+ if (bad) { *len = (sp_int)k; return i; }
1402
+ i += (sp_int)k;
1403
+ }
1404
+ *len = 0;
1405
+ return bl;
1406
+ }
1407
+ /* A replacement scrub's block answered: valid UTF-8, or CRuby's
1408
+ ArgumentError naming it. */
1409
+ const char *sp_str_scrub_repl(const char *r) {
1410
+ sp_int len, bl = (sp_int)sp_str_byte_len(r);
1411
+ if (sp_str_scrub_bad(r, bl, 0, &len) < bl)
1412
+ sp_raise_cls("ArgumentError", sp_sprintf("replacement must be valid byte sequence '%s'", sp_str_inspect(r)));
1413
+ return r;
1414
+ }
1415
+ /* String#scrub: an ASCII-8BIT receiver has no invalid sequence, so CRuby
1416
+ answers a copy of it, still ASCII-8BIT, without looking at the
1417
+ replacement; any other receiver is walked as UTF-8 (sp_str_scrub_utf8). */
1418
+ const char *sp_str_scrub(const char *s, const char *repl) {
1419
+ if (s && sp_str_is_binary(s)) return sp_str_dup(s);
1420
+ return sp_str_scrub_utf8(s, repl);
1421
+ }
1422
+ /* each invalid UTF-8 sequence (sp_str_scrub_bad) is replaced; a NULL
1423
+ replacement is U+FFFD (3 UTF-8 bytes: EF BF BD), as in CRuby. encode's
1424
+ UTF-8 to UTF-8 conversion calls it whatever the receiver's flag. */
1425
+ const char *sp_str_scrub_utf8(const char *s, const char *repl) {SP_GC_ROOT_STR(s);SP_GC_ROOT_STR(repl);
1426
+ if(!s)sp_nil_recv("scrub");
1427
+ static const char fffd[] = "\xEF\xBF\xBD";
1428
+ const char *r = repl ? repl : fffd;
1429
+ size_t rlen = strlen(r);
1430
+ sp_int bl = (sp_int)sp_str_byte_len(s);
1431
+ size_t cap = (size_t)bl + 64;
1432
+ /* malloc scratch (grown with realloc on invalid-byte runs); the final
1433
+ string is emitted at the exact length below. */
1434
+ char *out = (char *)malloc(cap);
1435
+ size_t olen = 0;
1436
+ sp_int i = 0;
1437
+ while (i < bl) {
1438
+ sp_int len, at = sp_str_scrub_bad(s, bl, i, &len);
1439
+ if (at < bl && repl && i == 0) sp_str_scrub_repl(repl); /* checked once, where it is used */
1440
+ size_t keep = (size_t)(at - i), add = at < bl ? rlen : 0;
1441
+ if (olen + keep + add + 1 >= cap) { cap = (olen + keep + add) * 2 + 64; out = (char *)realloc(out, cap); }
1442
+ memcpy(out + olen, s + i, keep);
1443
+ olen += keep;
1444
+ memcpy(out + olen, r, add);
1445
+ olen += add;
1446
+ i = at + len;
1447
+ if (at >= bl) break;
1448
+ }
1449
+ char *res = sp_str_alloc(olen);
1450
+ memcpy(res, out, olen);
1451
+ free(out);
1452
+ return res;
1453
+ }
1454
+ const char*sp_str_ljust(const char*s,sp_int w){SP_GC_ROOT_STR(s);if(!s)sp_nil_recv("ljust");sp_int cl=sp_str_length(s);if(cl>=w)return sp_str_dup(s);size_t bl=sp_str_byte_len(s);const char*e=sp_justify_size_err(bl,(uint64_t)(w-cl));if(e)sp_justify_size_raise(e);size_t pad=(size_t)(w-cl);char*r=sp_str_alloc_raw(bl+pad+1);memcpy(r,s,bl);memset(r+bl,' ',pad);r[bl+pad]=0;sp_str_set_len(r,bl+pad);return r;}
1455
+ const char*sp_str_rjust(const char*s,sp_int w){SP_GC_ROOT_STR(s);if(!s)sp_nil_recv("rjust");sp_int cl=sp_str_length(s);if(cl>=w)return sp_str_dup(s);size_t bl=sp_str_byte_len(s);const char*e=sp_justify_size_err(bl,(uint64_t)(w-cl));if(e)sp_justify_size_raise(e);size_t pad=(size_t)(w-cl);char*r=sp_str_alloc_raw(bl+pad+1);memset(r,' ',pad);memcpy(r+pad,s,bl);r[bl+pad]=0;sp_str_set_len(r,bl+pad);return r;}
1456
+ const char*sp_str_center(const char*s,sp_int w){SP_GC_ROOT_STR(s);if(!s)sp_nil_recv("center");sp_int cl=sp_str_length(s);if(cl>=w)return sp_str_dup(s);size_t bl=sp_str_byte_len(s);const char*e=sp_justify_size_err(bl,(uint64_t)(w-cl));if(e)sp_justify_size_raise(e);sp_int pad=w-cl;sp_int left=pad/2;sp_int right=pad-left;char*r=sp_str_alloc_raw(bl+pad+1);memset(r,' ',left);memcpy(r+left,s,bl);memset(r+left+bl,' ',right);r[bl+pad]=0;sp_str_set_len(r,bl+(size_t)pad);return r;}
1457
+ const char*sp_str_ljust2(const char*s,sp_int w,const char*pad){SP_GC_ROOT_STR(s);SP_GC_ROOT_STR(pad);if(!s)sp_nil_recv("ljust");/* an empty pad is rejected whatever the width, before the no-op case */if(!pad||sp_str_byte_len(pad)==0)sp_raise_cls("ArgumentError","zero width padding");sp_int cl=sp_str_length(s);if(cl>=w)return sp_str_dup(s);size_t bl=sp_str_byte_len(s);size_t pn;uint32_t*pcps=sp_utf8_decode_all(pad,&pn);if(pn==0){free(pcps);char*r=sp_str_alloc_raw(bl+1);memcpy(r,s,bl+1);sp_str_set_len(r,bl);return r;}sp_int need=w-cl;const char*e=sp_justify_size_err(bl,sp_pad_bytes(pcps,pn,(uint64_t)need));if(e){free(pcps);sp_justify_size_raise(e);}size_t padb=(size_t)sp_pad_bytes(pcps,pn,(uint64_t)need);char*r=sp_str_alloc_raw(bl+padb+1);memcpy(r,s,bl);size_t n=bl;for(sp_int i=0;i<need;i++)n+=sp_utf8_encode(pcps[i%pn],r+n);r[n]=0;sp_str_set_len(r,n);free(pcps);return r;}
1458
+ const char*sp_str_rjust2(const char*s,sp_int w,const char*pad){SP_GC_ROOT_STR(s);SP_GC_ROOT_STR(pad);if(!s)sp_nil_recv("rjust");/* an empty pad is rejected whatever the width, before the no-op case */if(!pad||sp_str_byte_len(pad)==0)sp_raise_cls("ArgumentError","zero width padding");sp_int cl=sp_str_length(s);if(cl>=w)return sp_str_dup(s);size_t bl=sp_str_byte_len(s);size_t pn;uint32_t*pcps=sp_utf8_decode_all(pad,&pn);if(pn==0){free(pcps);char*r=sp_str_alloc_raw(bl+1);memcpy(r,s,bl+1);sp_str_set_len(r,bl);return r;}sp_int need=w-cl;const char*e=sp_justify_size_err(bl,sp_pad_bytes(pcps,pn,(uint64_t)need));if(e){free(pcps);sp_justify_size_raise(e);}size_t padb=(size_t)sp_pad_bytes(pcps,pn,(uint64_t)need);char*r=sp_str_alloc_raw(bl+padb+1);size_t n=0;for(sp_int i=0;i<need;i++)n+=sp_utf8_encode(pcps[i%pn],r+n);memcpy(r+n,s,bl);r[n+bl]=0;sp_str_set_len(r,n+bl);free(pcps);return r;}
1459
+ const char*sp_str_center2(const char*s,sp_int w,const char*pad){SP_GC_ROOT_STR(s);SP_GC_ROOT_STR(pad);if(!s)sp_nil_recv("center");/* an empty pad is rejected whatever the width, before the no-op case */if(!pad||sp_str_byte_len(pad)==0)sp_raise_cls("ArgumentError","zero width padding");sp_int cl=sp_str_length(s);if(cl>=w)return sp_str_dup(s);size_t bl=sp_str_byte_len(s);size_t pn;uint32_t*pcps=sp_utf8_decode_all(pad,&pn);if(pn==0){free(pcps);char*r=sp_str_alloc_raw(bl+1);memcpy(r,s,bl+1);sp_str_set_len(r,bl);return r;}sp_int pd=w-cl;sp_int left=pd/2;sp_int right=pd-left;uint64_t lb64=sp_pad_bytes(pcps,pn,(uint64_t)left),rb64=sp_pad_bytes(pcps,pn,(uint64_t)right);const char*e=sp_justify_size_err(bl,(lb64==UINT64_MAX||rb64==UINT64_MAX||lb64>UINT64_MAX-rb64)?UINT64_MAX:lb64+rb64);if(e){free(pcps);sp_justify_size_raise(e);}size_t leftb=(size_t)lb64,rightb=(size_t)rb64;char*r=sp_str_alloc_raw(leftb+bl+rightb+1);size_t n=0;for(sp_int i=0;i<left;i++)n+=sp_utf8_encode(pcps[i%pn],r+n);memcpy(r+n,s,bl);n+=bl;for(sp_int i=0;i<right;i++)n+=sp_utf8_encode(pcps[i%pn],r+n);r[n]=0;sp_str_set_len(r,n);free(pcps);return r;}
1460
+ sp_int sp_str_index_opt(const char *s, const char *sub) {SP_GC_ROOT_STR(s);SP_GC_ROOT_STR(sub); sp_int n = sp_str_index(s, sub); return n < 0 ? SP_INT_NIL : n; }
1461
+ sp_int sp_str_index_from_opt(const char *s, const char *sub, sp_int start) {SP_GC_ROOT_STR(s);SP_GC_ROOT_STR(sub); sp_int n = sp_str_index_from(s, sub, start); return n < 0 ? SP_INT_NIL : n; }
1462
+ sp_int sp_str_rindex_opt(const char *s, const char *sub) {SP_GC_ROOT_STR(s);SP_GC_ROOT_STR(sub); sp_int n = sp_str_rindex(s, sub); return n < 0 ? SP_INT_NIL : n; }