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(crate) use install::declare_types;
16pub use install::{declare, install};
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::as_struct;
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/// `slice` / `subarray`: only the kept range is copied.
193fn slice(
194    caller: &mut Caller<'_, StoreData>,
195    receiver: &Val,
196    start: f64,
197    end: f64,
198    name: &str,
199) -> wasmtime::Result<Vec<u8>> {
200    let arr = uint8_array_backing(caller, receiver, name)?;
201    let len = arr.len(&mut *caller)? as i32;
202    let si = norm_clamp(start, len);
203    let count = (norm_clamp(end, len) - si).max(0) as usize;
204    read_uint8_array_range(caller, arr, si as usize, count, name)
205}
206
207/// `with(index, value)` → a copy with `index` replaced; `None` when out of range
208/// ([`install`] raises the catchable `RangeError("index out of range")`).
209fn with(bytes: &[u8], index: f64, value: f64) -> Option<Vec<u8>> {
210    let i = at_index(index, bytes.len() as i32)?;
211    let mut out = bytes.to_vec();
212    out[i] = to_uint8(value);
213    Some(out)
214}
215
216/// The byte equal to `target`, which a search compares as a number: a value
217/// that isn't an integer from 0 to 255 equals no byte.
218fn searched_byte(target: f64) -> Option<u8> {
219    (target.fract() == 0.0 && (0.0..=255.0).contains(&target)).then_some(target as u8)
220}
221
222fn index_of(bytes: &[u8], target: f64, from: f64) -> f64 {
223    let len = bytes.len() as i32;
224    let Some(target) = searched_byte(target) else {
225        return -1.0;
226    };
227    let mut i = fwd_from(from, len);
228    while i < len {
229        if bytes[i as usize] == target {
230            return f64::from(i);
231        }
232        i += 1;
233    }
234    -1.0
235}
236
237fn last_index_of(bytes: &[u8], target: f64, from: f64) -> f64 {
238    let len = bytes.len() as i32;
239    let Some(target) = searched_byte(target) else {
240        return -1.0;
241    };
242    let Some(mut i) = last_from(from, len) else {
243        return -1.0;
244    };
245    while i >= 0 {
246        if bytes[i as usize] == target {
247            return f64::from(i);
248        }
249        i -= 1;
250    }
251    -1.0
252}
253
254fn includes(bytes: &[u8], target: f64, from: f64) -> bool {
255    index_of(bytes, target, from) >= 0.0
256}
257
258/// Bytes formatted as decimals and joined by `sep` (`toString` passes `","`).
259/// Shared by the `join`/`toString` host fns and the vtable's `toString` slot.
260pub(crate) fn join(bytes: &[u8], sep: &[u16]) -> Vec<u16> {
261    let mut out: Vec<u16> = Vec::new();
262    for (i, &b) in bytes.iter().enumerate() {
263        if i > 0 {
264            out.extend_from_slice(sep);
265        }
266        out.extend(format_number_js(f64::from(b)).encode_utf16());
267    }
268    out
269}
270
271fn equals(a: &[u8], b: &[u8]) -> bool {
272    a == b
273}
274
275// ---------------------------------------------------------------------------
276// In-place mutators (overwrite the receiver backing, return the receiver)
277// ---------------------------------------------------------------------------
278
279fn reverse(
280    caller: &mut Caller<'_, StoreData>,
281    receiver: &Val,
282    mut bytes: Vec<u8>,
283) -> wasmtime::Result<Val> {
284    bytes.reverse();
285    store_bytes(caller, receiver, &bytes)?;
286    Ok(*receiver)
287}
288
289fn fill(
290    caller: &mut Caller<'_, StoreData>,
291    receiver: &Val,
292    value: f64,
293    start: f64,
294    end: f64,
295) -> wasmtime::Result<Val> {
296    let raw = backing(caller, receiver)?;
297    let len = i32::try_from(raw.len(&mut *caller)?).map_err(fatal_host_error)?;
298    let start = norm_clamp(start, len) as u32;
299    let end = norm_clamp(end, len) as u32;
300    let count = end.saturating_sub(start);
301    fuel::charge(&mut *caller, fuel::COPY, u64::from(count))?;
302    let chunk = [to_uint8(value); 4096];
303    let mut offset = start;
304    while offset < end {
305        let count = (end - offset).min(chunk.len() as u32);
306        let bytes = chunk
307            .get(..count as usize)
308            .ok_or_else(|| fatal_host_error("invalid fill chunk"))?;
309        raw.write_i8(&mut *caller, offset, bytes)?;
310        offset += count;
311    }
312    Ok(*receiver)
313}
314
315fn copy_within(
316    caller: &mut Caller<'_, StoreData>,
317    receiver: &Val,
318    target: f64,
319    start: f64,
320    end: f64,
321) -> wasmtime::Result<Val> {
322    let raw = backing(caller, receiver)?;
323    let len = i32::try_from(raw.len(&mut *caller)?).map_err(fatal_host_error)?;
324    let target = norm_clamp(target, len) as usize;
325    let start = norm_clamp(start, len) as usize;
326    let end = norm_clamp(end, len) as usize;
327    let count = end.saturating_sub(start).min(len as usize - target);
328    // A snapshot of only the requested span preserves overlapping copies.
329    let bytes = read_uint8_array_range(caller, raw, start, count, "Uint8Array#copyWithin")?;
330    fuel::charge(&mut *caller, fuel::COPY, count as u64)?;
331    raw.write_i8(&mut *caller, target as u32, &bytes)?;
332    Ok(*receiver)
333}
334
335/// Validate both ranges before copying or changing the receiver.
336fn set(
337    caller: &mut Caller<'_, StoreData>,
338    receiver: &Val,
339    source: &Val,
340    offset: f64,
341) -> wasmtime::Result<()> {
342    let target = backing(caller, receiver)?;
343    let source = backing(caller, source)?;
344    let target_len = target.len(&mut *caller)?;
345    let source_len = source.len(&mut *caller)?;
346    let offset = trunc_sat(offset);
347    if offset < 0
348        || (offset as u32)
349            .checked_add(source_len)
350            .is_none_or(|end| end > target_len)
351    {
352        return Err(crate::runtime::host::range_error("offset is out of bounds"));
353    }
354    let bytes = read_uint8_array_range(
355        caller,
356        source,
357        0,
358        source_len as usize,
359        "Uint8Array#set source",
360    )?;
361    fuel::charge(&mut *caller, fuel::COPY, u64::from(source_len))?;
362    target.write_i8(&mut *caller, offset as u32, &bytes)
363}
364
365fn allocate(caller: &mut Caller<'_, StoreData>, len: usize) -> wasmtime::Result<Val> {
366    use wasmtime::ArrayRefPre;
367    let intr = intrinsic_types(&mut *caller)?;
368    fuel::charge(&mut *caller, fuel::COPY, len as u64)?;
369    let len = u32::try_from(len).map_err(fatal_host_error)?;
370    let pre = ArrayRefPre::new(&mut *caller, intr.raw_uint8_array.clone());
371    let raw = ArrayRef::new_zeroed_i8(&mut *caller, &pre, len)?;
372    let vtable = crate::runtime::host::host_uint8_array_vtable(caller)?;
373    let pre = StructRefPre::new(&mut *caller, intr.uint8_array.clone());
374    let value = StructRef::new(
375        &mut *caller,
376        &pre,
377        &[vtable, Val::AnyRef(Some(raw.to_anyref()))],
378    )?;
379    Ok(Val::AnyRef(Some(value.to_anyref())))
380}
381
382async fn sort_bytes(
383    caller: &mut Caller<'_, StoreData>,
384    bytes: &mut Vec<u8>,
385    cmp: Option<&Closure>,
386) -> wasmtime::Result<()> {
387    let Some(cmp) = cmp else {
388        bytes.sort_unstable();
389        return Ok(());
390    };
391    if bytes.len() < 2 {
392        return Ok(());
393    }
394    // Every box stays alive until the host call returns, so each byte value is
395    // boxed once rather than once per comparison.
396    let mut boxes = Vec::with_capacity(256);
397    for byte in 0..=u8::MAX {
398        boxes.push(box_byte(caller, byte)?);
399    }
400    merge_sort(caller, bytes, cmp, |_, byte| {
401        boxes
402            .get(usize::from(byte))
403            .copied()
404            .ok_or_else(|| fatal_host_error("Uint8Array#sort: byte has no box"))
405    })
406    .await
407}
408
409async fn sort(
410    caller: &mut Caller<'_, StoreData>,
411    receiver: &Val,
412    mut bytes: Vec<u8>,
413    cmp: Option<Closure>,
414) -> wasmtime::Result<Val> {
415    sort_bytes(caller, &mut bytes, cmp.as_ref()).await?;
416    store_bytes(caller, receiver, &bytes)?;
417    Ok(*receiver)
418}
419
420// ---------------------------------------------------------------------------
421// Immutable variants (build a fresh result; receiver untouched)
422// ---------------------------------------------------------------------------
423
424fn to_reversed(mut bytes: Vec<u8>) -> Vec<u8> {
425    bytes.reverse();
426    bytes
427}
428
429async fn to_sorted(
430    caller: &mut Caller<'_, StoreData>,
431    mut bytes: Vec<u8>,
432    cmp: Option<Closure>,
433) -> wasmtime::Result<Vec<u8>> {
434    sort_bytes(caller, &mut bytes, cmp.as_ref()).await?;
435    Ok(bytes)
436}
437
438// ---------------------------------------------------------------------------
439// Higher-order methods (box each byte, re-enter a guest closure)
440// ---------------------------------------------------------------------------
441
442async fn for_each(
443    caller: &mut Caller<'_, StoreData>,
444    array: Val,
445    bytes: Vec<u8>,
446    f: &Closure,
447) -> wasmtime::Result<()> {
448    let f = ElementCallback::new(caller, f, Some(array))?;
449    for (index, b) in bytes.into_iter().enumerate() {
450        let boxed = box_byte(caller, b)?;
451        f.call(caller, None, boxed, index).await?;
452    }
453    Ok(())
454}
455
456async fn map(
457    caller: &mut Caller<'_, StoreData>,
458    array: Val,
459    bytes: Vec<u8>,
460    f: &Closure,
461) -> wasmtime::Result<Vec<u8>> {
462    let f = ElementCallback::new(caller, f, Some(array))?;
463    let mut out = Vec::with_capacity(bytes.len());
464    for (index, b) in bytes.into_iter().enumerate() {
465        let boxed = box_byte(caller, b)?;
466        let r = f.call(caller, None, boxed, index).await?;
467        out.push(unbox_byte(caller, &r, "Uint8Array#map")?);
468    }
469    Ok(out)
470}
471
472async fn filter(
473    caller: &mut Caller<'_, StoreData>,
474    array: Val,
475    bytes: Vec<u8>,
476    pred: &Closure,
477) -> wasmtime::Result<Vec<u8>> {
478    let pred = ElementCallback::new(caller, pred, Some(array))?;
479    let mut out = Vec::new();
480    for (index, b) in bytes.into_iter().enumerate() {
481        let boxed = box_byte(caller, b)?;
482        if pred.test(caller, boxed, index).await? {
483            out.push(b);
484        }
485    }
486    Ok(out)
487}
488
489async fn reduce(
490    caller: &mut Caller<'_, StoreData>,
491    array: Val,
492    bytes: Vec<u8>,
493    f: &Closure,
494    mut acc: Val,
495    reverse: bool,
496) -> wasmtime::Result<Val> {
497    let f = ElementCallback::new(caller, f, Some(array))?;
498    let order: Vec<usize> = if reverse {
499        (0..bytes.len()).rev().collect()
500    } else {
501        (0..bytes.len()).collect()
502    };
503    // The next iteration boxes its byte before it passes the accumulator on.
504    let kept_acc = KeptValue::new(caller)?;
505    for i in order {
506        let boxed = box_byte(caller, bytes[i])?;
507        acc = f.call(caller, Some(acc), boxed, i).await?;
508        kept_acc.set(caller, acc)?;
509    }
510    Ok(acc)
511}
512
513async fn some(
514    caller: &mut Caller<'_, StoreData>,
515    array: Val,
516    bytes: Vec<u8>,
517    pred: &Closure,
518) -> wasmtime::Result<bool> {
519    let pred = ElementCallback::new(caller, pred, Some(array))?;
520    for (index, b) in bytes.into_iter().enumerate() {
521        let boxed = box_byte(caller, b)?;
522        if pred.test(caller, boxed, index).await? {
523            return Ok(true);
524        }
525    }
526    Ok(false)
527}
528
529async fn every(
530    caller: &mut Caller<'_, StoreData>,
531    array: Val,
532    bytes: Vec<u8>,
533    pred: &Closure,
534) -> wasmtime::Result<bool> {
535    let pred = ElementCallback::new(caller, pred, Some(array))?;
536    for (index, b) in bytes.into_iter().enumerate() {
537        let boxed = box_byte(caller, b)?;
538        if !pred.test(caller, boxed, index).await? {
539            return Ok(false);
540        }
541    }
542    Ok(true)
543}
544
545/// Index of the first (or, with `reverse`, last) byte matching `pred`.
546async fn find_match(
547    caller: &mut Caller<'_, StoreData>,
548    array: Val,
549    bytes: &[u8],
550    pred: &Closure,
551    reverse: bool,
552) -> wasmtime::Result<Option<usize>> {
553    let pred = ElementCallback::new(caller, pred, Some(array))?;
554    let order: Vec<usize> = if reverse {
555        (0..bytes.len()).rev().collect()
556    } else {
557        (0..bytes.len()).collect()
558    };
559    for i in order {
560        let boxed = box_byte(caller, bytes[i])?;
561        if pred.test(caller, boxed, i).await? {
562            return Ok(Some(i));
563        }
564    }
565    Ok(None)
566}
567
568// ---------------------------------------------------------------------------
569// base64 / hex codecs
570// ---------------------------------------------------------------------------
571
572/// Standard padded base64 — the `$Uint8Array` `toJson` form.
573pub(crate) fn to_base64_standard(bytes: &[u8]) -> String {
574    encode_base64(bytes, false, false)
575}
576
577fn encode_base64(bytes: &[u8], url_safe: bool, omit_padding: bool) -> String {
578    let alpha = if url_safe {
579        &alphabet::URL_SAFE
580    } else {
581        &alphabet::STANDARD
582    };
583    let config: GeneralPurposeConfig = if omit_padding { NO_PAD } else { PAD };
584    GeneralPurpose::new(alpha, config).encode(bytes)
585}
586
587/// Accept canonical padding or no padding without decoding the input twice.
588fn decode_base64(s: &str, url_safe: bool) -> wasmtime::Result<Vec<u8>> {
589    let alpha = if url_safe {
590        &alphabet::URL_SAFE
591    } else {
592        &alphabet::STANDARD
593    };
594    let config = if s.as_bytes().last() == Some(&b'=') {
595        PAD
596    } else {
597        NO_PAD
598    };
599    GeneralPurpose::new(alpha, config)
600        .decode(s.as_bytes())
601        .map_err(|e| crate::runtime::host::syntax_error(format!("Uint8Array.fromBase64: {e}")))
602}
603
604fn to_hex(bytes: &[u8]) -> String {
605    let mut out = String::with_capacity(bytes.len() * 2);
606    for &b in bytes {
607        out.push(hex_digit(b >> 4));
608        out.push(hex_digit(b & 0xf));
609    }
610    out
611}
612
613fn hex_digit(nibble: u8) -> char {
614    char::from(if nibble < 10 {
615        b'0' + nibble
616    } else {
617        b'a' + (nibble - 10)
618    })
619}
620
621/// Parse a hex string (case-insensitive). Throws the prelude's messages on an odd
622/// length or an invalid digit so the error fixtures match.
623fn from_hex(units: &[u16]) -> wasmtime::Result<Vec<u8>> {
624    if !units.len().is_multiple_of(2) {
625        return Err(crate::runtime::host::syntax_error(
626            "Uint8Array.fromHex: string must have an even number of characters",
627        ));
628    }
629    let mut out = Vec::with_capacity(units.len() / 2);
630    for pair in units.as_chunks::<2>().0 {
631        let hi = hex_nibble(pair[0])?;
632        let lo = hex_nibble(pair[1])?;
633        out.push((hi << 4) | lo);
634    }
635    Ok(out)
636}
637
638fn hex_nibble(c: u16) -> wasmtime::Result<u8> {
639    match c {
640        0x30..=0x39 => Ok((c - 0x30) as u8),
641        0x61..=0x66 => Ok((c - 0x61 + 10) as u8),
642        0x41..=0x46 => Ok((c - 0x41 + 10) as u8),
643        _ => Err(crate::runtime::host::syntax_error(
644            "Uint8Array.fromHex: invalid hex digit",
645        )),
646    }
647}
648
649/// Read `Base64Options` — `(url_safe, omit_padding)`. An undefined/absent options object
650/// means standard alphabet, padded.
651fn read_base64_options(
652    caller: &mut Caller<'_, StoreData>,
653    opt: &Val,
654) -> wasmtime::Result<(bool, bool)> {
655    if super::undefined::is_undefined(caller, opt)? {
656        return Ok((false, false));
657    }
658    let (mut url_safe, mut omit_padding) = (false, false);
659    for (name, value) in read_object_entries(caller, opt, "Base64Options")? {
660        // An optional field the caller omitted is materialized as `undefined`; leave
661        // the default for it.
662        if super::undefined::is_undefined(caller, &value)? {
663            continue;
664        }
665        match String::from_utf16_lossy(&name).as_str() {
666            "alphabet" => {
667                let alphabet = read_string_units(caller, &value, "Base64Options.alphabet")?;
668                url_safe = String::from_utf16_lossy(&alphabet) == "base64url";
669            }
670            "omitPadding" => omit_padding = read_boolean(caller, &value)?,
671            _ => {}
672        }
673    }
674    Ok((url_safe, omit_padding))
675}
676
677fn read_boolean(caller: &mut Caller<'_, StoreData>, val: &Val) -> wasmtime::Result<bool> {
678    let st = as_struct(caller, val, "Base64Options.omitPadding")?;
679    match st.field(&mut *caller, 1)? {
680        Val::I32(b) => Ok(b != 0),
681        other => wasmtime::bail!("Base64Options.omitPadding: field 1 is {other:?}, not i32"),
682    }
683}
684
685// ---------------------------------------------------------------------------
686// Constructor statics
687// ---------------------------------------------------------------------------
688
689/// The byte count `Uint8Array.alloc(n)` / `new Uint8Array(n)` reserve, or a
690/// `RangeError` when `n` is not a length at all.
691///
692/// Truncation toward zero matches JS (`2.7` → 2, `NaN` → 0). The two rejected
693/// ends are the ones where saturating instead would be silently wrong: `f64 as
694/// u32` saturates a large positive to `u32::MAX` — a 4 GiB request — and a
695/// negative to `0`, an empty buffer where the program asked for a sized one.
696/// JS raises `RangeError: Invalid typed array length` for both.
697pub(crate) fn alloc_len(n: f64) -> wasmtime::Result<usize> {
698    // Truncate before testing the sign, the way JS's `ToIndex` does: `-0.5`
699    // truncates to `0` and is a legal empty length, while `-1` is not.
700    if n.trunc() < 0.0 {
701        return Err(crate::runtime::host::range_error(format!(
702            "invalid Uint8Array length {} — a length cannot be negative",
703            format_number_js(n)
704        )));
705    }
706    if n > MAX_ALLOC_LEN as f64 {
707        return Err(crate::runtime::host::range_error(format!(
708            "invalid Uint8Array length {} — the maximum is {MAX_ALLOC_LEN}",
709            format_number_js(n)
710        )));
711    }
712    Ok(n as u32 as usize)
713}
714
715/// Ceiling on a single `Uint8Array` allocation. Well past any real payload and
716/// far below the point where the request is a denial of service in itself; the
717/// store's `ResourceLimiter` still caps the aggregate.
718pub(crate) const MAX_ALLOC_LEN: usize = 1 << 30;
719
720/// Read an `$Array` of boxed numbers into bytes — backs `new`/`of`/`fromArray`.
721fn read_number_array(
722    caller: &mut Caller<'_, StoreData>,
723    val: &Val,
724    name: &str,
725) -> wasmtime::Result<Vec<u8>> {
726    let elements = crate::runtime::prelude::array::read_array(caller, val, name)?;
727    let mut bytes = Vec::with_capacity(elements.len());
728    for e in &elements {
729        bytes.push(unbox_byte(caller, e, name)?);
730    }
731    Ok(bytes)
732}
733
734#[cfg(test)]
735mod tests {
736    use super::*;
737
738    fn units(s: &str) -> Vec<u16> {
739        s.encode_utf16().collect()
740    }
741
742    #[test]
743    fn base64_round_trips_both_alphabets() {
744        let bytes = [0xfb_u8, 0xff, 0x00, 0x10, 0x41];
745        for url_safe in [false, true] {
746            let padded = encode_base64(&bytes, url_safe, false);
747            assert_eq!(decode_base64(&padded, url_safe).unwrap(), bytes);
748            // Decoding is forgiving on padding: an unpadded form still decodes.
749            let unpadded = encode_base64(&bytes, url_safe, true);
750            assert_eq!(decode_base64(&unpadded, url_safe).unwrap(), bytes);
751        }
752        // url-safe encodes 0xfb 0xff as `-_`, standard as `+/`.
753        assert!(encode_base64(&[0xfb, 0xff], true, false).contains('-'));
754        assert!(encode_base64(&[0xfb, 0xff], false, false).contains('+'));
755    }
756
757    #[test]
758    fn base64_padding_selection_preserves_previous_acceptance() {
759        // Enumerate short padding, alphabet and trailing-bit combinations
760        // against the previous two-decoder acceptance rule.
761        let alphabet = b"AQf=_-";
762        for length in 0..=5_u32 {
763            for mut code in 0..alphabet.len().pow(length) {
764                let mut input = Vec::new();
765                for _ in 0..length {
766                    input.push(alphabet[code % alphabet.len()]);
767                    code /= alphabet.len();
768                }
769                let text = std::str::from_utf8(&input).unwrap();
770                for url_safe in [false, true] {
771                    let alpha = if url_safe {
772                        &alphabet::URL_SAFE
773                    } else {
774                        &alphabet::STANDARD
775                    };
776                    let old = GeneralPurpose::new(alpha, PAD)
777                        .decode(&input)
778                        .or_else(|_| GeneralPurpose::new(alpha, NO_PAD).decode(&input));
779                    let new = decode_base64(text, url_safe);
780                    assert_eq!(new.ok(), old.ok(), "{text:?}, url_safe={url_safe}");
781                }
782            }
783        }
784    }
785
786    #[test]
787    fn hex_round_trips_and_rejects_bad_input() {
788        let bytes = [0x00_u8, 0x0f, 0xa0, 0xff];
789        assert_eq!(to_hex(&bytes), "000fa0ff");
790        assert_eq!(from_hex(&units("000fA0Ff")).unwrap(), bytes);
791        assert!(
792            from_hex(&units("0"))
793                .unwrap_err()
794                .to_string()
795                .contains("even number of characters")
796        );
797        assert!(
798            from_hex(&units("0g"))
799                .unwrap_err()
800                .to_string()
801                .contains("invalid hex digit")
802        );
803    }
804}