submilli_engine/runtime/prelude/string/
mod.rs1mod gc_text;
12mod install;
13pub(crate) mod search;
14mod transforms;
15
16pub(crate) use install::declare_types;
17pub use install::{declare, install};
18
19use unicode_normalization::UnicodeNormalization;
20
21use crate::runtime::host::fatal_host_error;
22
23pub struct Str(Vec<u16>);
27
28impl Str {
29 pub fn from_units(units: Vec<u16>) -> Self {
30 Self(units)
31 }
32
33 pub fn units(&self) -> &[u16] {
34 &self.0
35 }
36
37 pub fn len(&self) -> usize {
38 self.0.len()
39 }
40
41 pub fn is_empty(&self) -> bool {
42 self.0.is_empty()
43 }
44}
45
46#[derive(Debug)]
49pub struct RangeError(&'static str);
50
51impl RangeError {
52 pub fn message(&self) -> &'static str {
53 self.0
54 }
55}
56
57pub type Result<T> = std::result::Result<T, RangeError>;
58
59const MAX_RESULT_UNITS: usize = 32 * 1024 * 1024;
63
64pub(crate) fn repeat_length(len: usize, count: f64) -> Result<usize> {
74 let n = count.trunc();
75 if n <= -1.0 {
76 return Err(RangeError("Invalid count value"));
77 }
78 if n.is_nan() || n < 1.0 || len == 0 {
79 return Ok(0);
80 }
81 len.checked_mul(n as usize)
83 .filter(|&total| total <= MAX_RESULT_UNITS)
84 .ok_or(RangeError("Invalid count value"))
85}
86
87fn trunc_sat_i32(x: f64) -> i32 {
91 x as i32
92}
93
94fn checked_len(n: usize) -> Result<usize> {
97 if n > MAX_RESULT_UNITS {
98 Err(RangeError("Invalid string length"))
99 } else {
100 Ok(n)
101 }
102}
103
104fn clamp_to_len(x: i32, len: usize) -> usize {
106 if x < 0 { 0 } else { (x as usize).min(len) }
107}
108
109fn normalize_slice_index(x: i32, len: usize) -> usize {
112 if x < 0 {
113 let from_end = x as i64 + len as i64;
114 if from_end < 0 {
115 0
116 } else {
117 (from_end as usize).min(len)
118 }
119 } else {
120 (x as usize).min(len)
121 }
122}
123
124fn raw_index_of(haystack: &[u16], needle: &[u16], from: usize) -> Option<usize> {
127 search::Search::new(needle, false).find(haystack, from.min(haystack.len()))
128}
129
130pub fn char_at(s: &Str, index: f64) -> Str {
132 let units = s.units();
133 match unit_index(units.len(), index) {
134 Some(i) => Str::from_units(vec![units[i]]),
135 None => Str::from_units(Vec::new()),
136 }
137}
138
139pub fn at(s: &Str, index: f64) -> Option<Str> {
143 let units = s.units();
144 at_index(units.len(), index).map(|i| Str::from_units(vec![units[i]]))
145}
146
147pub fn char_code_at(s: &Str, index: f64) -> f64 {
149 let units = s.units();
150 unit_index(units.len(), index).map_or(f64::NAN, |i| units[i] as f64)
151}
152
153pub fn slice(s: &Str, start: f64, end: f64) -> Str {
156 let units = s.units();
157 Str::from_units(units[slice_range(units.len(), start, end)].to_vec())
158}
159
160pub fn substring(s: &Str, start: f64, end: f64) -> Str {
163 let units = s.units();
164 Str::from_units(units[substring_range(units.len(), start, end)].to_vec())
165}
166
167pub fn index_of(s: &Str, search: &Str, from: f64) -> f64 {
169 let haystack = s.units();
170 let from = clamp_to_len(trunc_sat_i32(from), haystack.len());
171 match raw_index_of(haystack, search.units(), from) {
172 Some(i) => i as f64,
173 None => -1.0,
174 }
175}
176
177pub fn last_index_of(s: &Str, search: &Str, from: f64) -> f64 {
179 let haystack = s.units();
180 let needle = search.units();
181 if needle.len() > haystack.len() {
182 return -1.0;
183 }
184 let max_start = haystack.len() - needle.len();
185 let from = clamp_to_len(trunc_sat_i32(from), max_start);
186 if needle.is_empty() {
187 return from as f64;
188 }
189 let end = from + needle.len();
190 search::Search::new(needle, true)
191 .find(&haystack[..end], 0)
192 .map_or(-1.0, |i| (end - i - needle.len()) as f64)
193}
194
195pub fn includes(s: &Str, search: &Str, from: f64) -> bool {
197 let haystack = s.units();
198 let from = clamp_to_len(trunc_sat_i32(from), haystack.len());
199 raw_index_of(haystack, search.units(), from).is_some()
200}
201
202pub fn starts_with(s: &Str, search: &Str, position: f64) -> bool {
204 let haystack = s.units();
205 let needle = search.units();
206 starts_with_window(haystack.len(), needle.len(), position)
207 .is_some_and(|window| &haystack[window] == needle)
208}
209
210pub fn ends_with(s: &Str, search: &Str, end_position: f64) -> bool {
212 let haystack = s.units();
213 let needle = search.units();
214 ends_with_window(haystack.len(), needle.len(), end_position)
215 .is_some_and(|window| &haystack[window] == needle)
216}
217
218pub(super) fn unit_index(len: usize, index: f64) -> Option<usize> {
220 let i = trunc_sat_i32(index);
221 (i >= 0 && (i as usize) < len).then_some(i as usize)
222}
223
224pub(super) fn at_index(len: usize, index: f64) -> Option<usize> {
226 let len = len as i64;
227 let mut i = trunc_sat_i32(index) as i64;
228 if i < 0 {
229 i += len;
230 }
231 (i >= 0 && i < len).then_some(i as usize)
232}
233
234pub(super) fn code_point(hi: u16, next: Option<u16>) -> f64 {
237 match next {
238 Some(lo) if (0xD800..=0xDBFF).contains(&hi) && (0xDC00..=0xDFFF).contains(&lo) => {
239 (0x10000 + (((hi as u32) - 0xD800) << 10) + ((lo as u32) - 0xDC00)) as f64
240 }
241 _ => hi as f64,
242 }
243}
244
245pub(super) fn slice_range(len: usize, start: f64, end: f64) -> std::ops::Range<usize> {
247 let si = normalize_slice_index(trunc_sat_i32(start), len);
248 let ei = normalize_slice_index(trunc_sat_i32(end), len);
249 si..ei.max(si)
250}
251
252pub(super) fn substring_range(len: usize, start: f64, end: f64) -> std::ops::Range<usize> {
255 let a = clamp_to_len(trunc_sat_i32(start), len);
256 let b = clamp_to_len(trunc_sat_i32(end), len);
257 a.min(b)..a.max(b)
258}
259
260pub(super) fn starts_with_window(
263 len: usize,
264 needle_len: usize,
265 position: f64,
266) -> Option<std::ops::Range<usize>> {
267 let pos = clamp_to_len(trunc_sat_i32(position), len);
268 (pos + needle_len <= len).then_some(pos..pos + needle_len)
269}
270
271pub(super) fn ends_with_window(
274 len: usize,
275 needle_len: usize,
276 end_position: f64,
277) -> Option<std::ops::Range<usize>> {
278 let end = clamp_to_len(trunc_sat_i32(end_position), len);
279 end.checked_sub(needle_len).map(|start| start..end)
280}
281
282pub fn eq(a: &Str, b: &Str) -> bool {
284 a.units() == b.units()
285}
286
287pub fn cmp(a: &Str, b: &Str) -> i32 {
291 for (&au, &bu) in a.units().iter().zip(b.units()) {
292 let diff = i32::from(au) - i32::from(bu);
293 if diff != 0 {
294 return diff;
295 }
296 }
297 a.len() as i32 - b.len() as i32
298}
299
300pub(crate) fn concat_length(left: usize, right: usize) -> wasmtime::Result<usize> {
302 left.checked_add(right)
303 .ok_or_else(|| fatal_host_error("string length overflow"))
304}
305
306pub(crate) fn pad_length(len: usize, target_length: f64, pad_len: usize) -> Result<usize> {
310 let target = trunc_sat_i32(target_length);
311 if target <= 0 || target as usize <= len || pad_len == 0 {
312 return Ok(len);
313 }
314 checked_len(target as usize)
315}
316
317pub fn is_well_formed(s: &Str) -> bool {
319 let u = s.units();
320 let mut i = 0;
321 while i < u.len() {
322 let c = u[i];
323 if (c & 0xFC00) == 0xD800 {
324 if i + 1 < u.len() && (u[i + 1] & 0xFC00) == 0xDC00 {
325 i += 2;
326 } else {
327 return false;
328 }
329 } else if (c & 0xFC00) == 0xDC00 {
330 return false;
331 } else {
332 i += 1;
333 }
334 }
335 true
336}
337
338pub fn to_well_formed(s: &Str) -> Str {
340 let mut out = s.units().to_vec();
341 let len = out.len();
342 let mut i = 0;
343 while i < len {
344 let c = out[i];
345 if (c & 0xFC00) == 0xD800 {
346 if i + 1 < len && (out[i + 1] & 0xFC00) == 0xDC00 {
347 i += 2;
348 } else {
349 out[i] = 0xFFFD;
350 i += 1;
351 }
352 } else if (c & 0xFC00) == 0xDC00 {
353 out[i] = 0xFFFD;
354 i += 1;
355 } else {
356 i += 1;
357 }
358 }
359 Str::from_units(out)
360}
361
362pub fn normalize(s: &Str, form: &Str) -> Result<Str> {
367 let form: fn(&str) -> String = match String::from_utf16_lossy(form.units()).as_str() {
368 "NFC" => |text| text.nfc().collect(),
369 "NFD" => |text| text.nfd().collect(),
370 "NFKC" => |text| text.nfkc().collect(),
371 "NFKD" => |text| text.nfkd().collect(),
372 _ => {
373 return Err(RangeError(
374 "String.normalize: invalid form — must be 'NFC', 'NFD', 'NFKC', or 'NFKD'",
375 ));
376 }
377 };
378 let mut out = Vec::with_capacity(s.units().len());
379 let mut run = String::new();
380 for scalar in char::decode_utf16(s.units().iter().copied()) {
381 match scalar {
382 Ok(c) => run.push(c),
383 Err(lone) => {
384 out.extend(form(&run).encode_utf16());
385 run.clear();
386 out.push(lone.unpaired_surrogate());
387 }
388 }
389 }
390 out.extend(form(&run).encode_utf16());
391 Ok(Str::from_units(out))
392}
393
394pub fn from_char_code(codes: &[f64]) -> Str {
397 Str::from_units(
398 codes
399 .iter()
400 .map(|&x| crate::runtime::number::to_uint16(x))
401 .collect(),
402 )
403}
404
405pub fn from_code_point(codes: &[f64]) -> Result<Str> {
408 let mut out = Vec::with_capacity(codes.len());
409 for &cpf in codes {
410 let cp = trunc_sat_i32(cpf);
411 if !(0..=0x10FFFF).contains(&cp) || cpf != cp as f64 {
412 return Err(RangeError("invalid code point"));
413 }
414 if cp < 0x10000 {
415 out.push(cp as u16);
416 } else {
417 let v = (cp - 0x10000) as u32;
418 out.push((0xD800 + (v >> 10)) as u16);
419 out.push((0xDC00 + (v & 0x3FF)) as u16);
420 }
421 }
422 Ok(Str::from_units(out))
423}
424
425#[cfg(test)]
426mod length_tests {
427 use super::*;
428
429 #[test]
430 fn length_validation_preserves_range_boundaries() {
431 assert_eq!(
432 repeat_length(1, MAX_RESULT_UNITS as f64).unwrap(),
433 MAX_RESULT_UNITS
434 );
435 for count in [
436 -1.0,
437 f64::NEG_INFINITY,
438 f64::INFINITY,
439 f64::MAX,
440 (MAX_RESULT_UNITS + 1) as f64,
441 ] {
442 assert!(repeat_length(1, count).is_err());
443 }
444 for count in [-0.5, 0.0, 0.5, f64::NAN] {
445 assert_eq!(repeat_length(1, count).unwrap(), 0);
446 }
447 assert_eq!(repeat_length(2, 2.9).unwrap(), 4);
448 assert_eq!(repeat_length(0, f64::INFINITY).unwrap(), 0);
449 assert!(repeat_length(usize::MAX, 2.0).is_err());
450 assert_eq!(
451 pad_length(1, MAX_RESULT_UNITS as f64, 1).unwrap(),
452 MAX_RESULT_UNITS
453 );
454 assert!(pad_length(1, (MAX_RESULT_UNITS + 1) as f64, 1).is_err());
455 assert_eq!(pad_length(2, f64::INFINITY, 0).unwrap(), 2);
456 assert_eq!(pad_length(2, f64::NAN, 1).unwrap(), 2);
457 assert_eq!(pad_length(2, -1.0, 1).unwrap(), 2);
458 assert_eq!(pad_length(2, 4.9, 1).unwrap(), 4);
459 }
460
461 #[test]
462 fn concat_length_overflow_is_fatal() {
463 assert_eq!(concat_length(2, 3).unwrap(), 5);
464 assert!(concat_length(usize::MAX, 1).is_err());
465 }
466}