Skip to main content

submilli_engine/runtime/prelude/uint8array/
mod.rs

1//! The submilli `Uint8Array` operations — the Rust port of the prelude's
2//! `Uint8Array` and `Uint8ArrayConstructor` surface. Bytes live as a `Vec<u8>`;
3//! the higher-order methods box each byte into a `$boxed_number` before calling
4//! back into a *guest* closure. The `$Uint8Array` marshalling and host-fn
5//! registration live in [`install`]; the shared
6//! [`Closure`](crate::runtime::prelude::closure::Closure) handles callbacks.
7//!
8//! In-place mutators (`reverse`/`fill`/`copyWithin`/`set`/`sort`) overwrite the
9//! receiver's existing `$rawUint8Array` backing in place and return the
10//! receiver: unlike `$Array`, the `$Uint8Array` struct's field 1 is an immutable
11//! reference, so the backing is never swapped.
12
13mod install;
14
15pub use install::{declare, install};
16pub(crate) use install::{declare_types, install_constants};
17
18use base64::Engine as _;
19use base64::alphabet;
20use base64::engine::general_purpose::{GeneralPurpose, GeneralPurposeConfig, NO_PAD, PAD};
21use wasmtime::{ArrayRef, Caller, Rooted, StructRef, StructRefPre, Val};
22
23use crate::runtime::StoreData;
24use crate::runtime::fuel;
25use crate::runtime::host::{
26    fatal_host_error, host_boxed_number_vtable, read_uint8, read_uint8_array_arg,
27    read_uint8_array_range, uint8_array_backing, write_submilli_uint8array_struct,
28};
29use crate::runtime::intrinsic_types::intrinsic_types;
30use crate::runtime::number::{format_number_js, to_uint8};
31use crate::runtime::prelude::array::{ElementCallback, merge_sort};
32use crate::runtime::prelude::closure::Closure;
33use crate::runtime::prelude::iterator::{IterKind, IteratorSource, as_struct, make_index_iterator};
34use crate::runtime::prelude::keep::KeptValue;
35use crate::runtime::prelude::vtable::{read_object_entries, read_string_units};
36
37// ---------------------------------------------------------------------------
38// Marshalling
39// ---------------------------------------------------------------------------
40
41/// Read a `$Uint8Array` (or bare `$rawUint8Array`) `Val` into its bytes — a local
42/// name so the method bodies that need the whole buffer open uniformly with
43/// `super::read_bytes`, mirroring the Array port's `read_array`; the accessors
44/// read in place instead. The actual struct/payload decode lives in `host`.
45pub(crate) fn read_bytes(
46    caller: &mut Caller<'_, StoreData>,
47    val: &Val,
48    name: &str,
49) -> wasmtime::Result<Vec<u8>> {
50    read_uint8_array_arg(caller, val, name)
51}
52
53/// Build a fresh `$Uint8Array` `Val` from bytes (reuses the host-owned vtable).
54fn build(caller: &mut Caller<'_, StoreData>, bytes: &[u8]) -> wasmtime::Result<Val> {
55    let st = write_submilli_uint8array_struct(caller, bytes)?;
56    Ok(Val::AnyRef(Some(st.to_anyref())))
57}
58
59/// Overwrite the receiver's `$rawUint8Array` backing from its start
60/// (`bytes.len()` must not exceed the backing length) — the in-place
61/// primitive for the mutators.
62fn store_bytes(
63    caller: &mut Caller<'_, StoreData>,
64    receiver: &Val,
65    bytes: &[u8],
66) -> wasmtime::Result<()> {
67    let raw = backing(caller, receiver)?;
68    fuel::charge(&mut *caller, fuel::COPY, bytes.len() as u64)?;
69    raw.write_i8(&mut *caller, 0, bytes)
70}
71
72/// The receiver's field-1 `$rawUint8Array` backing.
73fn backing(
74    caller: &mut Caller<'_, StoreData>,
75    receiver: &Val,
76) -> wasmtime::Result<Rooted<ArrayRef>> {
77    let st = as_struct(caller, receiver, "Uint8Array mutate receiver")?;
78    match st.field(&mut *caller, 1)? {
79        Val::AnyRef(Some(arr)) => arr.unwrap_array(&mut *caller),
80        other => Err(wasmtime::Error::msg(format!(
81            "Uint8Array: malformed $rawUint8Array backing {other:?}"
82        ))),
83    }
84}
85
86/// Box a byte into a `$boxed_number` for a callback argument or boxed return.
87fn box_byte(caller: &mut Caller<'_, StoreData>, b: u8) -> wasmtime::Result<Val> {
88    let boxed = intrinsic_types(&mut *caller)?.boxed_number.clone();
89    let vtable = host_boxed_number_vtable(caller)?;
90    let pre = StructRefPre::new(&mut *caller, boxed);
91    let st = StructRef::new(
92        &mut *caller,
93        &pre,
94        &[vtable, Val::F64(f64::from(b).to_bits())],
95    )?;
96    Ok(Val::AnyRef(Some(st.to_anyref())))
97}
98
99/// Unbox a `$boxed_number` callback result back to a byte (`map`).
100fn unbox_byte(caller: &mut Caller<'_, StoreData>, v: &Val, name: &str) -> wasmtime::Result<u8> {
101    let Val::AnyRef(Some(any)) = v else {
102        return Err(wasmtime::Error::msg(format!(
103            "{name} callback returned {v:?}, expected a number"
104        )));
105    };
106    let st = any
107        .as_struct(&mut *caller)?
108        .ok_or_else(|| wasmtime::Error::msg(format!("{name} callback result is not a number")))?;
109    match st.field(&mut *caller, 1)? {
110        Val::F64(bits) => Ok(to_uint8(f64::from_bits(bits))),
111        other => Err(wasmtime::Error::msg(format!(
112            "{name} callback result field was {other:?}, not f64"
113        ))),
114    }
115}
116
117// ---------------------------------------------------------------------------
118// Index math (ported 1:1 from the Wasm bodies; identical to the Array port)
119// ---------------------------------------------------------------------------
120
121fn trunc_sat(x: f64) -> i32 {
122    x as i32
123}
124
125fn norm_clamp(x: f64, len: i32) -> i32 {
126    let mut i = trunc_sat(x);
127    if i < 0 {
128        i += len;
129    }
130    i.clamp(0, len)
131}
132
133fn at_index(x: f64, len: i32) -> Option<usize> {
134    let mut i = trunc_sat(x);
135    if i < 0 {
136        i += len;
137    }
138    if i < 0 || i >= len {
139        None
140    } else {
141        Some(i as usize)
142    }
143}
144
145fn fwd_from(x: f64, len: i32) -> i32 {
146    let mut fi = trunc_sat(x);
147    if fi < 0 {
148        fi += len;
149    }
150    fi.max(0)
151}
152
153fn last_from(x: f64, len: i32) -> Option<i32> {
154    let mut fi = trunc_sat(x);
155    if fi < 0 {
156        fi += len;
157    }
158    if fi < 0 {
159        return None;
160    }
161    Some(fi.min(len - 1))
162}
163
164// ---------------------------------------------------------------------------
165// Accessors / pure value methods
166// ---------------------------------------------------------------------------
167
168/// `length` / `byteLength`, read off the backing without copying it.
169fn length(caller: &mut Caller<'_, StoreData>, receiver: &Val, name: &str) -> wasmtime::Result<f64> {
170    let arr = uint8_array_backing(caller, receiver, name)?;
171    Ok(f64::from(arr.len(&mut *caller)?))
172}
173
174/// `at(index)` → the byte boxed as a `$boxed_number`, or `undefined` when out of range.
175fn at(
176    caller: &mut Caller<'_, StoreData>,
177    receiver: &Val,
178    index: f64,
179    name: &str,
180) -> wasmtime::Result<Val> {
181    let arr = uint8_array_backing(caller, receiver, name)?;
182    let len = arr.len(&mut *caller)?;
183    match at_index(index, len as i32) {
184        Some(i) => {
185            let byte = read_uint8(caller, arr, i)?;
186            box_byte(caller, byte)
187        }
188        None => super::undefined::value(caller),
189    }
190}
191
192/// The `keys`/`values`/`entries` step: the index and the byte at `pos` of the
193/// live receiver, so bytes written during iteration are seen.
194fn byte_step(
195    caller: &mut Caller<'_, StoreData>,
196    payload: &Val,
197    pos: i32,
198) -> wasmtime::Result<Option<(Val, Val)>> {
199    let arr = uint8_array_backing(caller, payload, "Uint8Array iterator")?;
200    let Ok(index) = usize::try_from(pos) else {
201        return Ok(None);
202    };
203    if index >= arr.len(&mut *caller)? as usize {
204        return Ok(None);
205    }
206    let byte = read_uint8(caller, arr, index)?;
207    let key = box_byte_index(caller, index)?;
208    Ok(Some((key, box_byte(caller, byte)?)))
209}
210
211fn box_byte_index(caller: &mut Caller<'_, StoreData>, index: usize) -> wasmtime::Result<Val> {
212    let st = crate::runtime::host::write_boxed_number_struct(caller, index as f64)?;
213    Ok(Val::AnyRef(Some(st.to_anyref())))
214}
215
216/// `values()`, the bytes as `Array.from` collects them.
217pub(crate) fn values(caller: &mut Caller<'_, StoreData>, receiver: &Val) -> wasmtime::Result<Val> {
218    iterate(caller, receiver, IterKind::Values)
219}
220
221/// `keys()`, `values()` (also `iterator()`) and `entries()`.
222fn iterate(
223    caller: &mut Caller<'_, StoreData>,
224    receiver: &Val,
225    kind: IterKind,
226) -> wasmtime::Result<Val> {
227    make_index_iterator(caller, IteratorSource::Bytes, *receiver, kind, byte_step)
228}
229
230/// `slice` / `subarray`: only the kept range is copied.
231fn slice(
232    caller: &mut Caller<'_, StoreData>,
233    receiver: &Val,
234    start: f64,
235    end: f64,
236    name: &str,
237) -> wasmtime::Result<Vec<u8>> {
238    let arr = uint8_array_backing(caller, receiver, name)?;
239    let len = arr.len(&mut *caller)? as i32;
240    let si = norm_clamp(start, len);
241    let count = (norm_clamp(end, len) - si).max(0) as usize;
242    read_uint8_array_range(caller, arr, si as usize, count, name)
243}
244
245/// `with(index, value)` → a copy with `index` replaced; `None` when out of range
246/// ([`install`] raises the catchable `RangeError("index out of range")`).
247fn with(bytes: &[u8], index: f64, value: f64) -> Option<Vec<u8>> {
248    let i = at_index(index, bytes.len() as i32)?;
249    let mut out = bytes.to_vec();
250    out[i] = to_uint8(value);
251    Some(out)
252}
253
254/// The byte equal to `target`, which a search compares as a number: a value
255/// that isn't an integer from 0 to 255 equals no byte.
256fn searched_byte(target: f64) -> Option<u8> {
257    (target.fract() == 0.0 && (0.0..=255.0).contains(&target)).then_some(target as u8)
258}
259
260fn index_of(bytes: &[u8], target: f64, from: f64) -> f64 {
261    let len = bytes.len() as i32;
262    let Some(target) = searched_byte(target) else {
263        return -1.0;
264    };
265    let mut i = fwd_from(from, len);
266    while i < len {
267        if bytes[i as usize] == target {
268            return f64::from(i);
269        }
270        i += 1;
271    }
272    -1.0
273}
274
275fn last_index_of(bytes: &[u8], target: f64, from: f64) -> f64 {
276    let len = bytes.len() as i32;
277    let Some(target) = searched_byte(target) else {
278        return -1.0;
279    };
280    let Some(mut i) = last_from(from, len) else {
281        return -1.0;
282    };
283    while i >= 0 {
284        if bytes[i as usize] == target {
285            return f64::from(i);
286        }
287        i -= 1;
288    }
289    -1.0
290}
291
292fn includes(bytes: &[u8], target: f64, from: f64) -> bool {
293    index_of(bytes, target, from) >= 0.0
294}
295
296/// Bytes formatted as decimals and joined by `sep` (`toString` passes `","`).
297/// Shared by the `join`/`toString` host fns and the vtable's `toString` slot.
298pub(crate) fn join(bytes: &[u8], sep: &[u16]) -> Vec<u16> {
299    let mut out: Vec<u16> = Vec::new();
300    for (i, &b) in bytes.iter().enumerate() {
301        if i > 0 {
302            out.extend_from_slice(sep);
303        }
304        out.extend(format_number_js(f64::from(b)).encode_utf16());
305    }
306    out
307}
308
309fn equals(a: &[u8], b: &[u8]) -> bool {
310    a == b
311}
312
313// ---------------------------------------------------------------------------
314// In-place mutators (overwrite the receiver backing, return the receiver)
315// ---------------------------------------------------------------------------
316
317fn reverse(
318    caller: &mut Caller<'_, StoreData>,
319    receiver: &Val,
320    mut bytes: Vec<u8>,
321) -> wasmtime::Result<Val> {
322    bytes.reverse();
323    store_bytes(caller, receiver, &bytes)?;
324    Ok(*receiver)
325}
326
327fn fill(
328    caller: &mut Caller<'_, StoreData>,
329    receiver: &Val,
330    value: f64,
331    start: f64,
332    end: f64,
333) -> wasmtime::Result<Val> {
334    let raw = backing(caller, receiver)?;
335    let len = i32::try_from(raw.len(&mut *caller)?).map_err(fatal_host_error)?;
336    let start = norm_clamp(start, len) as u32;
337    let end = norm_clamp(end, len) as u32;
338    let count = end.saturating_sub(start);
339    fuel::charge(&mut *caller, fuel::COPY, u64::from(count))?;
340    let chunk = [to_uint8(value); 4096];
341    let mut offset = start;
342    while offset < end {
343        let count = (end - offset).min(chunk.len() as u32);
344        let bytes = chunk
345            .get(..count as usize)
346            .ok_or_else(|| fatal_host_error("invalid fill chunk"))?;
347        raw.write_i8(&mut *caller, offset, bytes)?;
348        offset += count;
349    }
350    Ok(*receiver)
351}
352
353fn copy_within(
354    caller: &mut Caller<'_, StoreData>,
355    receiver: &Val,
356    target: f64,
357    start: f64,
358    end: f64,
359) -> wasmtime::Result<Val> {
360    let raw = backing(caller, receiver)?;
361    let len = i32::try_from(raw.len(&mut *caller)?).map_err(fatal_host_error)?;
362    let target = norm_clamp(target, len) as usize;
363    let start = norm_clamp(start, len) as usize;
364    let end = norm_clamp(end, len) as usize;
365    let count = end.saturating_sub(start).min(len as usize - target);
366    // A snapshot of only the requested span preserves overlapping copies.
367    let bytes = read_uint8_array_range(caller, raw, start, count, "Uint8Array#copyWithin")?;
368    fuel::charge(&mut *caller, fuel::COPY, count as u64)?;
369    raw.write_i8(&mut *caller, target as u32, &bytes)?;
370    Ok(*receiver)
371}
372
373/// Validate both ranges before copying or changing the receiver.
374fn set(
375    caller: &mut Caller<'_, StoreData>,
376    receiver: &Val,
377    source: &Val,
378    offset: f64,
379) -> wasmtime::Result<()> {
380    let target = backing(caller, receiver)?;
381    let source = backing(caller, source)?;
382    let target_len = target.len(&mut *caller)?;
383    let source_len = source.len(&mut *caller)?;
384    let offset = trunc_sat(offset);
385    if offset < 0
386        || (offset as u32)
387            .checked_add(source_len)
388            .is_none_or(|end| end > target_len)
389    {
390        return Err(crate::runtime::host::range_error("offset is out of bounds"));
391    }
392    let bytes = read_uint8_array_range(
393        caller,
394        source,
395        0,
396        source_len as usize,
397        "Uint8Array#set source",
398    )?;
399    fuel::charge(&mut *caller, fuel::COPY, u64::from(source_len))?;
400    target.write_i8(&mut *caller, offset as u32, &bytes)
401}
402
403fn allocate(caller: &mut Caller<'_, StoreData>, len: usize) -> wasmtime::Result<Val> {
404    use wasmtime::ArrayRefPre;
405    let intr = intrinsic_types(&mut *caller)?;
406    fuel::charge(&mut *caller, fuel::COPY, len as u64)?;
407    let len = u32::try_from(len).map_err(fatal_host_error)?;
408    let pre = ArrayRefPre::new(&mut *caller, intr.raw_uint8_array.clone());
409    let raw = ArrayRef::new_zeroed_i8(&mut *caller, &pre, len)?;
410    let vtable = crate::runtime::host::host_uint8_array_vtable(caller)?;
411    let pre = StructRefPre::new(&mut *caller, intr.uint8_array.clone());
412    let value = StructRef::new(
413        &mut *caller,
414        &pre,
415        &[vtable, Val::AnyRef(Some(raw.to_anyref()))],
416    )?;
417    Ok(Val::AnyRef(Some(value.to_anyref())))
418}
419
420async fn sort_bytes(
421    caller: &mut Caller<'_, StoreData>,
422    bytes: &mut Vec<u8>,
423    cmp: Option<&Closure>,
424) -> wasmtime::Result<()> {
425    let Some(cmp) = cmp else {
426        bytes.sort_unstable();
427        return Ok(());
428    };
429    if bytes.len() < 2 {
430        return Ok(());
431    }
432    // Every box stays alive until the host call returns, so each byte value is
433    // boxed once rather than once per comparison.
434    let mut boxes = Vec::with_capacity(256);
435    for byte in 0..=u8::MAX {
436        boxes.push(box_byte(caller, byte)?);
437    }
438    merge_sort(caller, bytes, cmp, |_, byte| {
439        boxes
440            .get(usize::from(byte))
441            .copied()
442            .ok_or_else(|| fatal_host_error("Uint8Array#sort: byte has no box"))
443    })
444    .await
445}
446
447async fn sort(
448    caller: &mut Caller<'_, StoreData>,
449    receiver: &Val,
450    mut bytes: Vec<u8>,
451    cmp: Option<Closure>,
452) -> wasmtime::Result<Val> {
453    sort_bytes(caller, &mut bytes, cmp.as_ref()).await?;
454    store_bytes(caller, receiver, &bytes)?;
455    Ok(*receiver)
456}
457
458// ---------------------------------------------------------------------------
459// Immutable variants (build a fresh result; receiver untouched)
460// ---------------------------------------------------------------------------
461
462fn to_reversed(mut bytes: Vec<u8>) -> Vec<u8> {
463    bytes.reverse();
464    bytes
465}
466
467async fn to_sorted(
468    caller: &mut Caller<'_, StoreData>,
469    mut bytes: Vec<u8>,
470    cmp: Option<Closure>,
471) -> wasmtime::Result<Vec<u8>> {
472    sort_bytes(caller, &mut bytes, cmp.as_ref()).await?;
473    Ok(bytes)
474}
475
476// ---------------------------------------------------------------------------
477// Higher-order methods (box each byte, re-enter a guest closure)
478// ---------------------------------------------------------------------------
479
480async fn for_each(
481    caller: &mut Caller<'_, StoreData>,
482    array: Val,
483    bytes: Vec<u8>,
484    f: &Closure,
485) -> wasmtime::Result<()> {
486    let f = ElementCallback::new(caller, f, Some(array))?;
487    for (index, b) in bytes.into_iter().enumerate() {
488        let boxed = box_byte(caller, b)?;
489        f.call(caller, None, boxed, index).await?;
490    }
491    Ok(())
492}
493
494async fn map(
495    caller: &mut Caller<'_, StoreData>,
496    array: Val,
497    bytes: Vec<u8>,
498    f: &Closure,
499) -> wasmtime::Result<Vec<u8>> {
500    let f = ElementCallback::new(caller, f, Some(array))?;
501    let mut out = Vec::with_capacity(bytes.len());
502    for (index, b) in bytes.into_iter().enumerate() {
503        let boxed = box_byte(caller, b)?;
504        let r = f.call(caller, None, boxed, index).await?;
505        out.push(unbox_byte(caller, &r, "Uint8Array#map")?);
506    }
507    Ok(out)
508}
509
510async fn filter(
511    caller: &mut Caller<'_, StoreData>,
512    array: Val,
513    bytes: Vec<u8>,
514    pred: &Closure,
515) -> wasmtime::Result<Vec<u8>> {
516    let pred = ElementCallback::new(caller, pred, Some(array))?;
517    let mut out = Vec::new();
518    for (index, b) in bytes.into_iter().enumerate() {
519        let boxed = box_byte(caller, b)?;
520        if pred.test(caller, boxed, index).await? {
521            out.push(b);
522        }
523    }
524    Ok(out)
525}
526
527async fn reduce(
528    caller: &mut Caller<'_, StoreData>,
529    array: Val,
530    bytes: Vec<u8>,
531    f: &Closure,
532    mut acc: Val,
533    reverse: bool,
534) -> wasmtime::Result<Val> {
535    let f = ElementCallback::new(caller, f, Some(array))?;
536    let order: Vec<usize> = if reverse {
537        (0..bytes.len()).rev().collect()
538    } else {
539        (0..bytes.len()).collect()
540    };
541    // The next iteration boxes its byte before it passes the accumulator on.
542    let kept_acc = KeptValue::new(caller)?;
543    for i in order {
544        let boxed = box_byte(caller, bytes[i])?;
545        acc = f.call(caller, Some(acc), boxed, i).await?;
546        kept_acc.set(caller, acc)?;
547    }
548    Ok(acc)
549}
550
551async fn some(
552    caller: &mut Caller<'_, StoreData>,
553    array: Val,
554    bytes: Vec<u8>,
555    pred: &Closure,
556) -> wasmtime::Result<bool> {
557    let pred = ElementCallback::new(caller, pred, Some(array))?;
558    for (index, b) in bytes.into_iter().enumerate() {
559        let boxed = box_byte(caller, b)?;
560        if pred.test(caller, boxed, index).await? {
561            return Ok(true);
562        }
563    }
564    Ok(false)
565}
566
567async fn every(
568    caller: &mut Caller<'_, StoreData>,
569    array: Val,
570    bytes: Vec<u8>,
571    pred: &Closure,
572) -> wasmtime::Result<bool> {
573    let pred = ElementCallback::new(caller, pred, Some(array))?;
574    for (index, b) in bytes.into_iter().enumerate() {
575        let boxed = box_byte(caller, b)?;
576        if !pred.test(caller, boxed, index).await? {
577            return Ok(false);
578        }
579    }
580    Ok(true)
581}
582
583/// Index of the first (or, with `reverse`, last) byte matching `pred`.
584async fn find_match(
585    caller: &mut Caller<'_, StoreData>,
586    array: Val,
587    bytes: &[u8],
588    pred: &Closure,
589    reverse: bool,
590) -> wasmtime::Result<Option<usize>> {
591    let pred = ElementCallback::new(caller, pred, Some(array))?;
592    let order: Vec<usize> = if reverse {
593        (0..bytes.len()).rev().collect()
594    } else {
595        (0..bytes.len()).collect()
596    };
597    for i in order {
598        let boxed = box_byte(caller, bytes[i])?;
599        if pred.test(caller, boxed, i).await? {
600            return Ok(Some(i));
601        }
602    }
603    Ok(None)
604}
605
606// ---------------------------------------------------------------------------
607// base64 / hex codecs
608// ---------------------------------------------------------------------------
609
610/// Standard padded base64 — the `$Uint8Array` `toJson` form.
611pub(crate) fn to_base64_standard(bytes: &[u8]) -> String {
612    encode_base64(bytes, false, false)
613}
614
615fn encode_base64(bytes: &[u8], url_safe: bool, omit_padding: bool) -> String {
616    let alpha = if url_safe {
617        &alphabet::URL_SAFE
618    } else {
619        &alphabet::STANDARD
620    };
621    let config: GeneralPurposeConfig = if omit_padding { NO_PAD } else { PAD };
622    GeneralPurpose::new(alpha, config).encode(bytes)
623}
624
625/// Accept canonical padding or no padding without decoding the input twice.
626fn decode_base64(s: &str, url_safe: bool) -> wasmtime::Result<Vec<u8>> {
627    let alpha = if url_safe {
628        &alphabet::URL_SAFE
629    } else {
630        &alphabet::STANDARD
631    };
632    let config = if s.as_bytes().last() == Some(&b'=') {
633        PAD
634    } else {
635        NO_PAD
636    };
637    GeneralPurpose::new(alpha, config)
638        .decode(s.as_bytes())
639        .map_err(|e| crate::runtime::host::syntax_error(format!("Uint8Array.fromBase64: {e}")))
640}
641
642fn to_hex(bytes: &[u8]) -> String {
643    let mut out = String::with_capacity(bytes.len() * 2);
644    for &b in bytes {
645        out.push(hex_digit(b >> 4));
646        out.push(hex_digit(b & 0xf));
647    }
648    out
649}
650
651fn hex_digit(nibble: u8) -> char {
652    char::from(if nibble < 10 {
653        b'0' + nibble
654    } else {
655        b'a' + (nibble - 10)
656    })
657}
658
659/// Parse a hex string (case-insensitive). Throws the prelude's messages on an odd
660/// length or an invalid digit so the error fixtures match.
661fn from_hex(units: &[u16]) -> wasmtime::Result<Vec<u8>> {
662    if !units.len().is_multiple_of(2) {
663        return Err(crate::runtime::host::syntax_error(
664            "Uint8Array.fromHex: string must have an even number of characters",
665        ));
666    }
667    let mut out = Vec::with_capacity(units.len() / 2);
668    for pair in units.as_chunks::<2>().0 {
669        let hi = hex_nibble(pair[0])?;
670        let lo = hex_nibble(pair[1])?;
671        out.push((hi << 4) | lo);
672    }
673    Ok(out)
674}
675
676fn hex_nibble(c: u16) -> wasmtime::Result<u8> {
677    match c {
678        0x30..=0x39 => Ok((c - 0x30) as u8),
679        0x61..=0x66 => Ok((c - 0x61 + 10) as u8),
680        0x41..=0x46 => Ok((c - 0x41 + 10) as u8),
681        _ => Err(crate::runtime::host::syntax_error(
682            "Uint8Array.fromHex: invalid hex digit",
683        )),
684    }
685}
686
687/// Read `Base64Options` — `(url_safe, omit_padding)`. An undefined/absent options object
688/// means standard alphabet, padded.
689fn read_base64_options(
690    caller: &mut Caller<'_, StoreData>,
691    opt: &Val,
692) -> wasmtime::Result<(bool, bool)> {
693    if super::undefined::is_undefined(caller, opt)? {
694        return Ok((false, false));
695    }
696    let (mut url_safe, mut omit_padding) = (false, false);
697    for (name, value) in read_object_entries(caller, opt, "Base64Options")? {
698        // An optional field the caller omitted is materialized as `undefined`; leave
699        // the default for it.
700        if super::undefined::is_undefined(caller, &value)? {
701            continue;
702        }
703        match String::from_utf16_lossy(&name).as_str() {
704            "alphabet" => {
705                let alphabet = read_string_units(caller, &value, "Base64Options.alphabet")?;
706                url_safe = String::from_utf16_lossy(&alphabet) == "base64url";
707            }
708            "omitPadding" => omit_padding = read_boolean(caller, &value)?,
709            _ => {}
710        }
711    }
712    Ok((url_safe, omit_padding))
713}
714
715fn read_boolean(caller: &mut Caller<'_, StoreData>, val: &Val) -> wasmtime::Result<bool> {
716    let st = as_struct(caller, val, "Base64Options.omitPadding")?;
717    match st.field(&mut *caller, 1)? {
718        Val::I32(b) => Ok(b != 0),
719        other => wasmtime::bail!("Base64Options.omitPadding: field 1 is {other:?}, not i32"),
720    }
721}
722
723// ---------------------------------------------------------------------------
724// Constructor statics
725// ---------------------------------------------------------------------------
726
727/// The byte count `Uint8Array.alloc(n)` / `new Uint8Array(n)` reserve, or a
728/// `RangeError` when `n` is not a length at all.
729///
730/// Truncation toward zero matches JS (`2.7` → 2, `NaN` → 0). The two rejected
731/// ends are the ones where saturating instead would be silently wrong: `f64 as
732/// u32` saturates a large positive to `u32::MAX` — a 4 GiB request — and a
733/// negative to `0`, an empty buffer where the program asked for a sized one.
734/// JS raises `RangeError: Invalid typed array length` for both.
735pub(crate) fn alloc_len(n: f64) -> wasmtime::Result<usize> {
736    // Truncate before testing the sign, the way JS's `ToIndex` does: `-0.5`
737    // truncates to `0` and is a legal empty length, while `-1` is not.
738    if n.trunc() < 0.0 {
739        return Err(crate::runtime::host::range_error(format!(
740            "invalid Uint8Array length {} — a length cannot be negative",
741            format_number_js(n)
742        )));
743    }
744    if n > MAX_ALLOC_LEN as f64 {
745        return Err(crate::runtime::host::range_error(format!(
746            "invalid Uint8Array length {} — the maximum is {MAX_ALLOC_LEN}",
747            format_number_js(n)
748        )));
749    }
750    Ok(n as u32 as usize)
751}
752
753/// Ceiling on a single `Uint8Array` allocation. Well past any real payload and
754/// far below the point where the request is a denial of service in itself; the
755/// store's `ResourceLimiter` still caps the aggregate.
756pub(crate) const MAX_ALLOC_LEN: usize = 1 << 30;
757
758/// Read an `$Array` of boxed numbers into bytes — backs `new`/`of`/`fromArray`.
759fn read_number_array(
760    caller: &mut Caller<'_, StoreData>,
761    val: &Val,
762    name: &str,
763) -> wasmtime::Result<Vec<u8>> {
764    let elements = crate::runtime::prelude::array::read_array(caller, val, name)?;
765    let mut bytes = Vec::with_capacity(elements.len());
766    for e in &elements {
767        bytes.push(unbox_byte(caller, e, name)?);
768    }
769    Ok(bytes)
770}
771
772#[cfg(test)]
773mod tests {
774    use super::*;
775
776    fn units(s: &str) -> Vec<u16> {
777        s.encode_utf16().collect()
778    }
779
780    #[test]
781    fn base64_round_trips_both_alphabets() {
782        let bytes = [0xfb_u8, 0xff, 0x00, 0x10, 0x41];
783        for url_safe in [false, true] {
784            let padded = encode_base64(&bytes, url_safe, false);
785            assert_eq!(decode_base64(&padded, url_safe).unwrap(), bytes);
786            // Decoding is forgiving on padding: an unpadded form still decodes.
787            let unpadded = encode_base64(&bytes, url_safe, true);
788            assert_eq!(decode_base64(&unpadded, url_safe).unwrap(), bytes);
789        }
790        // url-safe encodes 0xfb 0xff as `-_`, standard as `+/`.
791        assert!(encode_base64(&[0xfb, 0xff], true, false).contains('-'));
792        assert!(encode_base64(&[0xfb, 0xff], false, false).contains('+'));
793    }
794
795    #[test]
796    fn base64_padding_selection_preserves_previous_acceptance() {
797        // Enumerate short padding, alphabet and trailing-bit combinations
798        // against the previous two-decoder acceptance rule.
799        let alphabet = b"AQf=_-";
800        for length in 0..=5_u32 {
801            for mut code in 0..alphabet.len().pow(length) {
802                let mut input = Vec::new();
803                for _ in 0..length {
804                    input.push(alphabet[code % alphabet.len()]);
805                    code /= alphabet.len();
806                }
807                let text = std::str::from_utf8(&input).unwrap();
808                for url_safe in [false, true] {
809                    let alpha = if url_safe {
810                        &alphabet::URL_SAFE
811                    } else {
812                        &alphabet::STANDARD
813                    };
814                    let old = GeneralPurpose::new(alpha, PAD)
815                        .decode(&input)
816                        .or_else(|_| GeneralPurpose::new(alpha, NO_PAD).decode(&input));
817                    let new = decode_base64(text, url_safe);
818                    assert_eq!(new.ok(), old.ok(), "{text:?}, url_safe={url_safe}");
819                }
820            }
821        }
822    }
823
824    #[test]
825    fn hex_round_trips_and_rejects_bad_input() {
826        let bytes = [0x00_u8, 0x0f, 0xa0, 0xff];
827        assert_eq!(to_hex(&bytes), "000fa0ff");
828        assert_eq!(from_hex(&units("000fA0Ff")).unwrap(), bytes);
829        assert!(
830            from_hex(&units("0"))
831                .unwrap_err()
832                .to_string()
833                .contains("even number of characters")
834        );
835        assert!(
836            from_hex(&units("0g"))
837                .unwrap_err()
838                .to_string()
839                .contains("invalid hex digit")
840        );
841    }
842}