1mod install;
12pub(crate) mod search;
13mod transforms;
14
15pub(crate) use install::declare_types;
16pub use install::{declare, install};
17
18use unicode_normalization::UnicodeNormalization;
19
20use crate::runtime::host::{fatal_host_error, range_error};
21use crate::runtime::limits::{HostBytes, TenantLimits};
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
46pub struct AdmittedStr {
49 value: Str,
51 _native: HostBytes,
52}
53
54impl AdmittedStr {
55 pub fn as_str(&self) -> &Str {
56 &self.value
57 }
58
59 fn allocate(limits: &TenantLimits, units: usize) -> wasmtime::Result<Self> {
60 let native = admit_units(limits, units)?;
61 let mut buffer = Vec::new();
62 buffer.try_reserve_exact(units).map_err(fatal_host_error)?;
63 Ok(Self {
64 value: Str::from_units(buffer),
65 _native: native,
66 })
67 }
68}
69
70fn admit_units(limits: &TenantLimits, units: usize) -> wasmtime::Result<HostBytes> {
71 let bytes = units
72 .checked_mul(std::mem::size_of::<u16>())
73 .and_then(|bytes| u64::try_from(bytes).ok())
74 .ok_or_else(|| fatal_host_error("native string byte length overflow"))?;
75 Ok(HostBytes::new(limits, bytes)?)
76}
77
78#[derive(Debug)]
81pub struct RangeError(&'static str);
82
83impl RangeError {
84 pub fn message(&self) -> &'static str {
85 self.0
86 }
87}
88
89pub type Result<T> = std::result::Result<T, RangeError>;
90
91const MAX_RESULT_UNITS: usize = 32 * 1024 * 1024;
95
96pub fn repeat(s: &Str, count: f64, limits: &TenantLimits) -> wasmtime::Result<AdmittedStr> {
104 let total = repeat_length(s.len(), count).map_err(|error| range_error(error.message()))?;
105 let mut out = AdmittedStr::allocate(limits, total)?;
106 if total == 0 {
107 return Ok(out);
108 }
109 for _ in 0..total / s.len() {
112 out.value.0.extend_from_slice(s.units());
113 }
114 Ok(out)
115}
116
117pub(crate) fn repeat_length(len: usize, count: f64) -> Result<usize> {
118 let n = count.trunc();
119 if n <= -1.0 {
120 return Err(RangeError("Invalid count value"));
121 }
122 if n.is_nan() || n < 1.0 || len == 0 {
123 return Ok(0);
124 }
125 len.checked_mul(n as usize)
127 .filter(|&total| total <= MAX_RESULT_UNITS)
128 .ok_or(RangeError("Invalid count value"))
129}
130
131fn trunc_sat_i32(x: f64) -> i32 {
135 x as i32
136}
137
138fn checked_len(n: usize) -> Result<usize> {
143 if n > MAX_RESULT_UNITS {
144 Err(RangeError("Invalid string length"))
145 } else {
146 Ok(n)
147 }
148}
149
150fn clamp_to_len(x: i32, len: usize) -> usize {
152 if x < 0 { 0 } else { (x as usize).min(len) }
153}
154
155fn normalize_slice_index(x: i32, len: usize) -> usize {
158 if x < 0 {
159 let from_end = x as i64 + len as i64;
160 if from_end < 0 {
161 0
162 } else {
163 (from_end as usize).min(len)
164 }
165 } else {
166 (x as usize).min(len)
167 }
168}
169
170fn raw_index_of(haystack: &[u16], needle: &[u16], from: usize) -> Option<usize> {
173 search::Search::new(needle, false).find(haystack, from.min(haystack.len()))
174}
175
176pub fn char_at(s: &Str, index: f64) -> Str {
178 let units = s.units();
179 match unit_index(units.len(), index) {
180 Some(i) => Str::from_units(vec![units[i]]),
181 None => Str::from_units(Vec::new()),
182 }
183}
184
185pub fn at(s: &Str, index: f64) -> Option<Str> {
189 let units = s.units();
190 at_index(units.len(), index).map(|i| Str::from_units(vec![units[i]]))
191}
192
193pub fn char_code_at(s: &Str, index: f64) -> f64 {
195 let units = s.units();
196 unit_index(units.len(), index).map_or(f64::NAN, |i| units[i] as f64)
197}
198
199pub fn slice(s: &Str, start: f64, end: f64) -> Str {
202 let units = s.units();
203 Str::from_units(units[slice_range(units.len(), start, end)].to_vec())
204}
205
206pub fn substring(s: &Str, start: f64, end: f64) -> Str {
209 let units = s.units();
210 Str::from_units(units[substring_range(units.len(), start, end)].to_vec())
211}
212
213pub fn index_of(s: &Str, search: &Str, from: f64) -> f64 {
215 let haystack = s.units();
216 let from = clamp_to_len(trunc_sat_i32(from), haystack.len());
217 match raw_index_of(haystack, search.units(), from) {
218 Some(i) => i as f64,
219 None => -1.0,
220 }
221}
222
223pub fn last_index_of(s: &Str, search: &Str, from: f64) -> f64 {
225 let haystack = s.units();
226 let needle = search.units();
227 if needle.len() > haystack.len() {
228 return -1.0;
229 }
230 let max_start = haystack.len() - needle.len();
231 let from = clamp_to_len(trunc_sat_i32(from), max_start);
232 if needle.is_empty() {
233 return from as f64;
234 }
235 let end = from + needle.len();
236 search::Search::new(needle, true)
237 .find(&haystack[..end], 0)
238 .map_or(-1.0, |i| (end - i - needle.len()) as f64)
239}
240
241pub fn includes(s: &Str, search: &Str, from: f64) -> bool {
243 let haystack = s.units();
244 let from = clamp_to_len(trunc_sat_i32(from), haystack.len());
245 raw_index_of(haystack, search.units(), from).is_some()
246}
247
248pub fn starts_with(s: &Str, search: &Str, position: f64) -> bool {
250 let haystack = s.units();
251 let needle = search.units();
252 starts_with_window(haystack.len(), needle.len(), position)
253 .is_some_and(|window| &haystack[window] == needle)
254}
255
256pub fn ends_with(s: &Str, search: &Str, end_position: f64) -> bool {
258 let haystack = s.units();
259 let needle = search.units();
260 ends_with_window(haystack.len(), needle.len(), end_position)
261 .is_some_and(|window| &haystack[window] == needle)
262}
263
264pub(super) fn unit_index(len: usize, index: f64) -> Option<usize> {
266 let i = trunc_sat_i32(index);
267 (i >= 0 && (i as usize) < len).then_some(i as usize)
268}
269
270pub(super) fn at_index(len: usize, index: f64) -> Option<usize> {
272 let len = len as i64;
273 let mut i = trunc_sat_i32(index) as i64;
274 if i < 0 {
275 i += len;
276 }
277 (i >= 0 && i < len).then_some(i as usize)
278}
279
280pub(super) fn code_point(hi: u16, next: Option<u16>) -> f64 {
283 match next {
284 Some(lo) if (0xD800..=0xDBFF).contains(&hi) && (0xDC00..=0xDFFF).contains(&lo) => {
285 (0x10000 + (((hi as u32) - 0xD800) << 10) + ((lo as u32) - 0xDC00)) as f64
286 }
287 _ => hi as f64,
288 }
289}
290
291pub(super) fn slice_range(len: usize, start: f64, end: f64) -> std::ops::Range<usize> {
293 let si = normalize_slice_index(trunc_sat_i32(start), len);
294 let ei = normalize_slice_index(trunc_sat_i32(end), len);
295 si..ei.max(si)
296}
297
298pub(super) fn substring_range(len: usize, start: f64, end: f64) -> std::ops::Range<usize> {
301 let a = clamp_to_len(trunc_sat_i32(start), len);
302 let b = clamp_to_len(trunc_sat_i32(end), len);
303 a.min(b)..a.max(b)
304}
305
306pub(super) fn starts_with_window(
309 len: usize,
310 needle_len: usize,
311 position: f64,
312) -> Option<std::ops::Range<usize>> {
313 let pos = clamp_to_len(trunc_sat_i32(position), len);
314 (pos + needle_len <= len).then_some(pos..pos + needle_len)
315}
316
317pub(super) fn ends_with_window(
320 len: usize,
321 needle_len: usize,
322 end_position: f64,
323) -> Option<std::ops::Range<usize>> {
324 let end = clamp_to_len(trunc_sat_i32(end_position), len);
325 end.checked_sub(needle_len).map(|start| start..end)
326}
327
328pub fn eq(a: &Str, b: &Str) -> bool {
330 a.units() == b.units()
331}
332
333pub fn cmp(a: &Str, b: &Str) -> i32 {
337 for (&au, &bu) in a.units().iter().zip(b.units()) {
338 let diff = i32::from(au) - i32::from(bu);
339 if diff != 0 {
340 return diff;
341 }
342 }
343 a.len() as i32 - b.len() as i32
344}
345
346pub fn concat(s: &Str, other: &Str, limits: &TenantLimits) -> wasmtime::Result<AdmittedStr> {
348 let total = concat_length(s.len(), other.len())?;
349 let mut out = AdmittedStr::allocate(limits, total)?;
350 out.value.0.extend_from_slice(s.units());
351 out.value.0.extend_from_slice(other.units());
352 Ok(out)
353}
354
355pub(crate) fn concat_length(left: usize, right: usize) -> wasmtime::Result<usize> {
356 left.checked_add(right)
357 .ok_or_else(|| fatal_host_error("native string length overflow"))
358}
359
360pub fn pad_start(
363 s: &Str,
364 target_length: f64,
365 pad: &Str,
366 limits: &TenantLimits,
367) -> wasmtime::Result<AdmittedStr> {
368 pad_string(s, target_length, pad, limits, true)
369}
370
371pub fn pad_end(
374 s: &Str,
375 target_length: f64,
376 pad: &Str,
377 limits: &TenantLimits,
378) -> wasmtime::Result<AdmittedStr> {
379 pad_string(s, target_length, pad, limits, false)
380}
381
382fn pad_string(
383 s: &Str,
384 target_length: f64,
385 pad: &Str,
386 limits: &TenantLimits,
387 leading: bool,
388) -> wasmtime::Result<AdmittedStr> {
389 let total = pad_length(s.len(), target_length, pad.len())
390 .map_err(|error| range_error(error.message()))?;
391 let mut out = AdmittedStr::allocate(limits, total)?;
392 if !leading {
393 out.value.0.extend_from_slice(s.units());
394 }
395 let mut remaining = total - s.len();
396 while remaining > 0 {
397 let chunk = remaining.min(pad.len());
400 out.value.0.extend_from_slice(&pad.units()[..chunk]);
401 remaining -= chunk;
402 }
403 if leading {
404 out.value.0.extend_from_slice(s.units());
405 }
406 Ok(out)
407}
408
409pub(crate) fn pad_length(len: usize, target_length: f64, pad_len: usize) -> Result<usize> {
410 let target = trunc_sat_i32(target_length);
411 if target <= 0 || target as usize <= len || pad_len == 0 {
412 return Ok(len);
413 }
414 checked_len(target as usize)
415}
416
417pub fn is_well_formed(s: &Str) -> bool {
419 let u = s.units();
420 let mut i = 0;
421 while i < u.len() {
422 let c = u[i];
423 if (c & 0xFC00) == 0xD800 {
424 if i + 1 < u.len() && (u[i + 1] & 0xFC00) == 0xDC00 {
425 i += 2;
426 } else {
427 return false;
428 }
429 } else if (c & 0xFC00) == 0xDC00 {
430 return false;
431 } else {
432 i += 1;
433 }
434 }
435 true
436}
437
438pub fn to_well_formed(s: &Str) -> Str {
440 let mut out = s.units().to_vec();
441 let len = out.len();
442 let mut i = 0;
443 while i < len {
444 let c = out[i];
445 if (c & 0xFC00) == 0xD800 {
446 if i + 1 < len && (out[i + 1] & 0xFC00) == 0xDC00 {
447 i += 2;
448 } else {
449 out[i] = 0xFFFD;
450 i += 1;
451 }
452 } else if (c & 0xFC00) == 0xDC00 {
453 out[i] = 0xFFFD;
454 i += 1;
455 } else {
456 i += 1;
457 }
458 }
459 Str::from_units(out)
460}
461
462pub use transforms::{to_lower_case, to_upper_case, trim, trim_end, trim_start};
463
464pub fn normalize(s: &Str, form: &Str) -> Result<Str> {
469 let form: fn(&str) -> String = match String::from_utf16_lossy(form.units()).as_str() {
470 "NFC" => |text| text.nfc().collect(),
471 "NFD" => |text| text.nfd().collect(),
472 "NFKC" => |text| text.nfkc().collect(),
473 "NFKD" => |text| text.nfkd().collect(),
474 _ => {
475 return Err(RangeError(
476 "String.normalize: invalid form — must be 'NFC', 'NFD', 'NFKC', or 'NFKD'",
477 ));
478 }
479 };
480 let mut out = Vec::with_capacity(s.units().len());
481 let mut run = String::new();
482 for scalar in char::decode_utf16(s.units().iter().copied()) {
483 match scalar {
484 Ok(c) => run.push(c),
485 Err(lone) => {
486 out.extend(form(&run).encode_utf16());
487 run.clear();
488 out.push(lone.unpaired_surrogate());
489 }
490 }
491 }
492 out.extend(form(&run).encode_utf16());
493 Ok(Str::from_units(out))
494}
495
496pub fn from_char_code(codes: &[f64]) -> Str {
499 Str::from_units(
500 codes
501 .iter()
502 .map(|&x| crate::runtime::number::to_uint16(x))
503 .collect(),
504 )
505}
506
507pub fn from_code_point(codes: &[f64]) -> Result<Str> {
510 let mut out = Vec::with_capacity(codes.len());
511 for &cpf in codes {
512 let cp = trunc_sat_i32(cpf);
513 if !(0..=0x10FFFF).contains(&cp) || cpf != cp as f64 {
514 return Err(RangeError("invalid code point"));
515 }
516 if cp < 0x10000 {
517 out.push(cp as u16);
518 } else {
519 let v = (cp - 0x10000) as u32;
520 out.push((0xD800 + (v >> 10)) as u16);
521 out.push((0xDC00 + (v & 0x3FF)) as u16);
522 }
523 }
524 Ok(Str::from_units(out))
525}
526
527#[cfg(test)]
528mod allocation_tests {
529 use super::*;
530 use crate::runtime::host::ends_the_run;
531 use crate::runtime::limits::MemoryCapExceeded;
532
533 #[test]
534 fn admitted_outputs_hold_and_refund_exact_bytes() {
535 let limits = TenantLimits::new(8);
536 let s = Str::from_units(vec![0xD800, 0x0061]);
537 let out = repeat(&s, 2.0, &limits).expect("exact output budget");
538 assert_eq!(out.as_str().units(), &[0xD800, 0x0061, 0xD800, 0x0061]);
539 assert_eq!(limits.host_attached_bytes(), 8);
540 assert!(repeat(&s, 1.0, &limits).is_err(), "live output is charged");
541 drop(out);
542 assert_eq!(limits.host_attached_bytes(), 0);
543 assert_eq!(limits.peak_bytes(), 8);
544 let smaller = TenantLimits::new(7);
545 let err = repeat(&s, 2.0, &smaller).err().expect("one byte short");
546 assert!(err.is::<MemoryCapExceeded>());
547 assert_eq!(smaller.host_attached_bytes(), 0);
548 assert_eq!(smaller.peak_bytes(), 0);
549 }
550
551 #[test]
552 fn all_builders_admit_before_filling() {
553 let limits = TenantLimits::new(6);
554 let s = Str::from_units(vec![0xD800]);
555 let pad = Str::from_units(vec![0xDC00, 0x0062]);
556 let start = pad_start(&s, 3.0, &pad, &limits).expect("padStart fits");
557 assert_eq!(start.as_str().units(), &[0xDC00, 0x0062, 0xD800]);
558 drop(start);
559 let end = pad_end(&s, 3.0, &pad, &limits).expect("padEnd fits");
560 assert_eq!(end.as_str().units(), &[0xD800, 0xDC00, 0x0062]);
561 drop(end);
562 let joined = concat(&s, &pad, &limits).expect("concat fits");
563 assert_eq!(joined.as_str().units(), &[0xD800, 0xDC00, 0x0062]);
564 drop(joined);
565 assert_eq!(limits.host_attached_bytes(), 0);
566 let smaller = TenantLimits::new(4);
567 assert!(pad_start(&s, 3.0, &pad, &smaller).is_err());
568 assert!(pad_end(&s, 3.0, &pad, &smaller).is_err());
569 assert!(concat(&s, &pad, &smaller).is_err());
570 assert_eq!(smaller.peak_bytes(), 0);
571 }
572
573 #[test]
574 fn allocation_failure_refunds_admission_and_is_fatal() {
575 let limits = TenantLimits::new(u64::MAX);
576 let err = AdmittedStr::allocate(&limits, usize::MAX / 2)
579 .err()
580 .expect("invalid allocation layout");
581 assert!(ends_the_run(&err));
582 assert_eq!(limits.host_attached_bytes(), 0);
583 assert!(limits.peak_bytes() > 0, "admission preceded reservation");
584 assert!(AdmittedStr::allocate(&limits, usize::MAX).is_err());
585 assert_eq!(limits.host_attached_bytes(), 0);
586 assert!(concat_length(usize::MAX, 1).is_err());
587 }
588
589 #[test]
590 fn length_validation_preserves_range_boundaries() {
591 assert_eq!(
592 repeat_length(1, MAX_RESULT_UNITS as f64).unwrap(),
593 MAX_RESULT_UNITS
594 );
595 for count in [
596 -1.0,
597 f64::NEG_INFINITY,
598 f64::INFINITY,
599 f64::MAX,
600 (MAX_RESULT_UNITS + 1) as f64,
601 ] {
602 assert!(repeat_length(1, count).is_err());
603 }
604 for count in [-0.5, 0.0, 0.5, f64::NAN] {
605 assert_eq!(repeat_length(1, count).unwrap(), 0);
606 }
607 assert_eq!(repeat_length(2, 2.9).unwrap(), 4);
608 assert_eq!(repeat_length(0, f64::INFINITY).unwrap(), 0);
609 assert!(repeat_length(usize::MAX, 2.0).is_err());
610 assert_eq!(
611 pad_length(1, MAX_RESULT_UNITS as f64, 1).unwrap(),
612 MAX_RESULT_UNITS
613 );
614 assert!(pad_length(1, (MAX_RESULT_UNITS + 1) as f64, 1).is_err());
615 assert_eq!(pad_length(2, f64::INFINITY, 0).unwrap(), 2);
616 assert_eq!(pad_length(2, f64::NAN, 1).unwrap(), 2);
617 assert_eq!(pad_length(2, -1.0, 1).unwrap(), 2);
618 assert_eq!(pad_length(2, 4.9, 1).unwrap(), 4);
619 }
620
621 #[test]
622 fn empty_and_unpadded_results_use_fallible_admission() {
623 let empty = Str::from_units(Vec::new());
624 let s = Str::from_units(vec![0xD800, 0x0061]);
625 let limits = TenantLimits::new(4);
626 let out = repeat(&empty, f64::INFINITY, &limits).unwrap();
627 assert!(out.as_str().is_empty());
628 assert_eq!(limits.host_attached_bytes(), 0);
629 drop(out);
630 for op in [pad_start, pad_end] {
631 let out = op(&s, f64::INFINITY, &empty, &limits).unwrap();
632 assert_eq!(out.as_str().units(), s.units());
633 assert_eq!(limits.host_attached_bytes(), 4);
634 drop(out);
635 assert!(op(&s, 1.0, &empty, &TenantLimits::new(3)).is_err());
636 }
637 let err = repeat(&s, -1.0, &limits).err().unwrap();
638 assert!(!ends_the_run(&err), "range errors remain catchable");
639 assert_eq!(limits.host_attached_bytes(), 0);
640 }
641}